From 5b4703d6153689bf008ffe37f43c489c4f9211a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Nov 2022 08:39:02 +1100 Subject: [PATCH 001/220] Added conversion from RGBa to RGB --- Tests/test_image_convert.py | 7 +++++++ src/libImaging/Convert.c | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 902d8bf8f..0a7202a33 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -104,6 +104,13 @@ def test_rgba_p(): assert_image_similar(im, comparable, 20) +def test_rgba(): + with Image.open("Tests/images/transparent.png") as im: + assert im.mode == "RGBA" + + assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) + + def test_trns_p(tmp_path): im = hopper("P") im.info["transparency"] = 0 diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 2b45d0cc4..b03bd02af 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -479,6 +479,25 @@ rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) { } } +static void +rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) { + int x; + unsigned int alpha; + for (x = 0; x < xsize; x++, in += 4) { + alpha = in[3]; + if (alpha == 255 || alpha == 0) { + *out++ = in[0]; + *out++ = in[1]; + *out++ = in[2]; + } else { + *out++ = CLIP8((255 * in[0]) / alpha); + *out++ = CLIP8((255 * in[1]) / alpha); + *out++ = CLIP8((255 * in[2]) / alpha); + } + *out++ = 255; + } +} + /* * Conversion of RGB + single transparent color to RGBA, * where any pixel that matches the color will have the @@ -934,6 +953,7 @@ static struct { {"RGBA", "HSV", rgb2hsv}, {"RGBa", "RGBA", rgba2rgbA}, + {"RGBa", "RGB", rgba2rgb_}, {"RGBX", "1", rgb2bit}, {"RGBX", "L", rgb2l}, From 6ddbe4cbf029a1d1c33cbd68683801864092cb47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Nov 2022 18:26:31 +1100 Subject: [PATCH 002/220] Added signed option when saving JPEG2000 images --- Tests/test_file_jpeg2k.py | 14 ++++++++++++++ docs/handbook/image-file-formats.rst | 5 +++++ src/PIL/Jpeg2KImagePlugin.py | 2 ++ src/encode.c | 5 ++++- src/libImaging/Jpeg2K.h | 3 +++ src/libImaging/Jpeg2KEncode.c | 2 +- 6 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index cd142e67f..0229b2243 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -252,6 +252,20 @@ def test_mct(): assert_image_similar(im, jp2, 1.0e-3) +def test_sgnd(tmp_path): + outfile = str(tmp_path / "temp.jp2") + + im = Image.new("L", (1, 1)) + im.save(outfile) + with Image.open(outfile) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + im = Image.new("L", (1, 1)) + im.save(outfile, signed=True) + with Image.open(outfile) as reloaded_signed: + assert reloaded_signed.getpixel((0, 0)) == 128 + + def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1e79db68b..93ae1b054 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -563,6 +563,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: encoded using RLCP mode will have increasing resolutions decoded as they arrive, and so on. +**signed** + If true, then tell the encoder to save the image as signed. + + .. versionadded:: 9.4.0 + **cinema_mode** Set the encoder to produce output compliant with the digital cinema specifications. The options here are ``"no"`` (the default), diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index c67d8d6bf..11d1d488a 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -321,6 +321,7 @@ def _save(im, fp, filename): progression = info.get("progression", "LRCP") cinema_mode = info.get("cinema_mode", "no") mct = info.get("mct", 0) + signed = info.get("signed", False) fd = -1 if hasattr(fp, "fileno"): @@ -342,6 +343,7 @@ def _save(im, fp, filename): progression, cinema_mode, mct, + signed, fd, ) diff --git a/src/encode.c b/src/encode.c index 72c7f64d0..aa47fe671 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1188,11 +1188,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char *cinema_mode = "no"; OPJ_CINEMA_MODE cine_mode; char mct = 0; + int sgnd = 0; Py_ssize_t fd = -1; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbn", + "ss|OOOsOnOOOssbbn", &mode, &format, &offset, @@ -1207,6 +1208,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &progression, &cinema_mode, &mct, + &sgnd, &fd)) { return NULL; } @@ -1305,6 +1307,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->progression = prog_order; context->cinema_mode = cine_mode; context->mct = mct; + context->sgnd = sgnd; return (PyObject *)encoder; } diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index d030b0c43..b28a0440a 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -85,6 +85,9 @@ typedef struct { /* Set multiple component transformation */ char mct; + /* Signed */ + int sgnd; + /* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */ OPJ_PROG_ORDER progression; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index fe5511ba5..db1c5c0c9 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -343,7 +343,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { image_params[n].x0 = image_params[n].y0 = 0; image_params[n].prec = prec; image_params[n].bpp = bpp; - image_params[n].sgnd = 0; + image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; } image = opj_image_create(components, image_params, color_space); From 13a4feafb75b1c0cdf4821dd6db88f0c44d9ce4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 16:38:50 +1100 Subject: [PATCH 003/220] Patch OpenJPEG to include uclouvain/openjpeg#1423 --- winbuild/build_prepare.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5277b84f8..9f1e74e53 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -323,6 +323,11 @@ deps = { "filename": "openjpeg-2.5.0.tar.gz", "dir": "openjpeg-2.5.0", "license": "LICENSE", + "patch": { + r"src\lib\openjp2\ht_dec.c": { + "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501 + } + }, "build": [ cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), cmd_nmake(target="clean"), From 73b91f58d0bfa387880ab94ccb20afd08f634c53 Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 15:10:05 +0000 Subject: [PATCH 004/220] Support arbitrary number of loaded modules on Windows Changed the TKinter module loading function for Windows to support the rare (but possible) case of having more than 1024 modules loaded. This is an adaptation of the same fix that was added to Matplotlib in [PR #22445](https://github.com/matplotlib/matplotlib/pull/22445). --- src/Tk/tkImaging.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 16b9a2edd..58ca23a56 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -310,7 +310,7 @@ load_tkinter_funcs(void) { * Return 0 for success, non-zero for failure. */ - HMODULE hMods[1024]; + HMODULE* hMods = NULL; HANDLE hProcess; DWORD cbNeeded; unsigned int i; @@ -327,33 +327,45 @@ load_tkinter_funcs(void) { /* Returns pseudo-handle that does not need to be closed */ hProcess = GetCurrentProcess(); + /* Allocate module handlers array */ + if (!EnumProcessModules(hProcess, NULL, 0, &cbNeeded)) { + PyErr_SetFromWindowsErr(0); + return 1; + } + if (!(hMods = static_cast(malloc(cbNeeded)))) { + PyErr_NoMemory(); + return 1; + } + /* Iterate through modules in this process looking for Tcl / Tk names */ - if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { + if (EnumProcessModules(hProcess, hMods, cbNeeded, &cbNeeded)) { for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { if (!found_tcl) { found_tcl = get_tcl(hMods[i]); if (found_tcl == -1) { - return 1; + goto exit; } } if (!found_tk) { found_tk = get_tk(hMods[i]); if (found_tk == -1) { - return 1; + goto exit; } } if (found_tcl && found_tk) { - return 0; + goto exit; } } } - if (found_tcl == 0) { +exit: + free(hMods); + if (found_tcl != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); - } else { + } else if (found_tk != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); } - return 1; + return int((found_tcl != 1) && (found_tk != 1)); } #else /* not Windows */ From 40d9732a40cc577c3cd914dc3d22f0ccdeec9a2e Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 15:57:07 +0000 Subject: [PATCH 005/220] Fix cast syntax --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 58ca23a56..68afb988a 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -332,7 +332,7 @@ load_tkinter_funcs(void) { PyErr_SetFromWindowsErr(0); return 1; } - if (!(hMods = static_cast(malloc(cbNeeded)))) { + if (!(hMods = (HMODULE*) malloc(cbNeeded))) { PyErr_NoMemory(); return 1; } From 80d7fa9004e35001a843a64f4accd0cb51e75813 Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 16:09:01 +0000 Subject: [PATCH 006/220] Fix another bad cast syntax --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 68afb988a..9b4b1d8f5 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -365,7 +365,7 @@ exit: } else if (found_tk != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); } - return int((found_tcl != 1) && (found_tk != 1)); + return (int) ((found_tcl != 1) && (found_tk != 1)); } #else /* not Windows */ From 4a36d9d761790d88c98e08dd273dbc7abb0c71b2 Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 22:27:18 +0000 Subject: [PATCH 007/220] Avoid using PyErr_SetFromWindowsErr on Cygwin --- src/Tk/tkImaging.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 9b4b1d8f5..e16f33eb0 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -329,7 +329,11 @@ load_tkinter_funcs(void) { /* Allocate module handlers array */ if (!EnumProcessModules(hProcess, NULL, 0, &cbNeeded)) { +#if defined(__CYGWIN__) + PyErr_SetString(PyExc_OSError, "Call to EnumProcessModules failed"); +#else PyErr_SetFromWindowsErr(0); +#endif return 1; } if (!(hMods = (HMODULE*) malloc(cbNeeded))) { From 406a8478cd73c5c166783e9d3583b2249e3b7068 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 17:41:06 +1100 Subject: [PATCH 008/220] Use break instead of goto --- src/Tk/tkImaging.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index e16f33eb0..506bb7008 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -347,22 +347,21 @@ load_tkinter_funcs(void) { if (!found_tcl) { found_tcl = get_tcl(hMods[i]); if (found_tcl == -1) { - goto exit; + break; } } if (!found_tk) { found_tk = get_tk(hMods[i]); if (found_tk == -1) { - goto exit; + break; } } if (found_tcl && found_tk) { - goto exit; + break; } } } -exit: free(hMods); if (found_tcl != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); From ccac8540771120bdeb570ec5b7bbfc4e3e9a38dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 21:33:09 +1100 Subject: [PATCH 009/220] If available, use wl-paste for grabclipboard() on Linux --- Tests/test_imagegrab.py | 10 +++++++--- src/PIL/ImageGrab.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 5e0eca28b..1ad4de63f 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -64,9 +64,13 @@ $bmp = New-Object Drawing.Bitmap 200, 200 ) p.communicate() else: - with pytest.raises(NotImplementedError) as e: - ImageGrab.grabclipboard() - assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" + if not shutil.which("wl-paste"): + with pytest.raises(NotImplementedError) as e: + ImageGrab.grabclipboard() + assert ( + str(e.value) + == "wl-paste is required for ImageGrab.grabclipboard() on Linux" + ) return ImageGrab.grabclipboard() diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 38074cb1b..12ad9ad71 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -132,4 +132,14 @@ def grabclipboard(): return BmpImagePlugin.DibImageFile(data) return None else: - raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only") + if not shutil.which("wl-paste"): + raise NotImplementedError( + "wl-paste is required for ImageGrab.grabclipboard() on Linux" + ) + fh, filepath = tempfile.mkstemp() + subprocess.call(["wl-paste"], stdout=fh) + os.close(fh) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + return im From 2ecf88eaa621266f63405ca7e1fdbdb7ed4d5c8d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 22:01:37 +1100 Subject: [PATCH 010/220] If available, use xclip for grabclipboard() on Linux --- Tests/test_imagegrab.py | 4 ++-- src/PIL/ImageGrab.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 1ad4de63f..01442dc69 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -68,8 +68,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200 with pytest.raises(NotImplementedError) as e: ImageGrab.grabclipboard() assert ( - str(e.value) - == "wl-paste is required for ImageGrab.grabclipboard() on Linux" + str(e.value) == "wl-paste or xclip is required" + " for ImageGrab.grabclipboard() on Linux" ) return diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 12ad9ad71..8cf956809 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -132,12 +132,16 @@ def grabclipboard(): return BmpImagePlugin.DibImageFile(data) return None else: - if not shutil.which("wl-paste"): + if shutil.which("wl-paste"): + args = ["wl-paste"] + elif shutil.which("xclip"): + args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] + else: raise NotImplementedError( - "wl-paste is required for ImageGrab.grabclipboard() on Linux" + "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" ) fh, filepath = tempfile.mkstemp() - subprocess.call(["wl-paste"], stdout=fh) + subprocess.call(args, stdout=fh) os.close(fh) im = Image.open(filepath) im.load() From f6f622dceee19fef36e6746a7943f2e806d8cabd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Dec 2022 06:36:27 +1100 Subject: [PATCH 011/220] Clarify apply_transparency() docstring --- src/PIL/Image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..155a546c2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1482,7 +1482,8 @@ class Image: def apply_transparency(self): """ If a P mode image has a "transparency" key in the info dictionary, - remove the key and apply the transparency to the palette instead. + remove the key and instead apply the transparency to the palette. + Otherwise, the image is unchanged. """ if self.mode != "P" or "transparency" not in self.info: return From 6da4169f3724ffe20c72d8ef4a2e0dc21815b343 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Dec 2022 22:40:55 +1100 Subject: [PATCH 012/220] Fixed writing int as ASCII tag --- Tests/test_file_tiff_metadata.py | 13 +++++++------ src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index b90dde3d9..48c0273fe 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -185,20 +185,21 @@ def test_iptc(tmp_path): im.save(out) -def test_writing_bytes_to_ascii(tmp_path): +def test_writing_other_types_to_ascii(tmp_path): im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] assert tag.type == TiffTags.ASCII - info[271] = b"test" - out = str(tmp_path / "temp.tiff") - im.save(out, tiffinfo=info) + for (value, expected) in {b"test": "test", 1: "1"}.items(): + info[271] = value - with Image.open(out) as reloaded: - assert reloaded.tag_v2[271] == "test" + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == expected def test_writing_int_to_bytes(tmp_path): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab9ac5ea2..791e692c1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -732,6 +732,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 + if isinstance(value, int): + value = str(value) if not isinstance(value, bytes): value = value.encode("ascii", "replace") return value + b"\0" From 08816f43ae621830cd4cf9dc1fecfbae63e5cc60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 15:46:14 +1100 Subject: [PATCH 013/220] Added support for I;16 modes in putdata() --- Tests/test_image_putdata.py | 5 +++-- src/_imaging.c | 30 +++++++++++++----------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 3d60e52a2..0e6293349 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -55,10 +55,11 @@ def test_mode_with_L_with_float(): assert im.getpixel((0, 0)) == 2 -def test_mode_i(): +@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) +def test_mode_i(mode): src = hopper("L") data = list(src.getdata()) - im = Image.new("I", src.size, 0) + im = Image.new(mode, src.size, 0) im.putdata(data, 2, 256) target = [2 * elt + 256 for elt in data] diff --git a/src/_imaging.c b/src/_imaging.c index 940b5fbb3..05e1370f6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1531,25 +1531,21 @@ if (PySequence_Check(op)) { \ PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } + int endian = strncmp(image->mode, "I;16", 4) == 0 ? (strcmp(image->mode, "I;16B") == 0 ? 2 : 1) : 0; double value; - if (scale == 1.0 && offset == 0.0) { - /* Clipped data */ - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - image->image8[y][x] = (UINT8)CLIP8(value); - if (++x >= (int)image->xsize) { - x = 0, y++; - } + for (i = x = y = 0; i < n; i++) { + set_value_to_item(seq, i); + if (scale != 1.0 || offset != 0.0) { + value = value * scale + offset; } - - } else { - /* Scaled and clipped data */ - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - image->image8[y][x] = CLIP8(value * scale + offset); - if (++x >= (int)image->xsize) { - x = 0, y++; - } + if (endian == 0) { + image->image8[y][x] = (UINT8)CLIP8(value); + } else { + image->image8[y][x * 2 + (endian == 2 ? 1 : 0)] = CLIP8((int)value % 256); + image->image8[y][x * 2 + (endian == 2 ? 0 : 1)] = CLIP8((int)value >> 8); + } + if (++x >= (int)image->xsize) { + x = 0, y++; } } PyErr_Clear(); /* Avoid weird exceptions */ From bcdb208fe2365b75c8d87d29783b9cc9f8cb683b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 09:44:53 +1100 Subject: [PATCH 014/220] Restored Image constants, except for duplicate Resampling attributes --- Tests/test_image.py | 10 ++-------- docs/deprecations.rst | 34 +++------------------------------- src/PIL/Image.py | 17 +++++++++-------- 3 files changed, 14 insertions(+), 47 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index a37c90296..a0c50b5f4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -921,12 +921,7 @@ class TestImage: with pytest.warns(DeprecationWarning): assert Image.CONTAINER == 2 - def test_constants_deprecation(self): - with pytest.warns(DeprecationWarning): - assert Image.NEAREST == 0 - with pytest.warns(DeprecationWarning): - assert Image.NONE == 0 - + def test_constants(self): with pytest.warns(DeprecationWarning): assert Image.LINEAR == Image.Resampling.BILINEAR with pytest.warns(DeprecationWarning): @@ -943,8 +938,7 @@ class TestImage: Image.Quantize, ): for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(Image, name) == enum[name] + assert getattr(Image, name) == enum[name] @pytest.mark.parametrize( "path", diff --git a/docs/deprecations.rst b/docs/deprecations.rst index dec652df8..bbd873800 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -77,37 +77,9 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` -``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` -``Image.ORDERED`` ``Image.Dither.ORDERED`` -``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` -``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` -``Image.WEB`` ``Image.Palette.WEB`` -``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` -``Image.AFFINE`` ``Image.Transform.AFFINE`` -``Image.EXTENT`` ``Image.Transform.EXTENT`` -``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` -``Image.QUAD`` ``Image.Transform.QUAD`` -``Image.MESH`` ``Image.Transform.MESH`` -``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` -``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` -``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` -``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` -``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` -``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` -``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` -``Image.BOX`` ``Image.Resampling.BOX`` -``Image.BILINEAR`` ``Image.Resampling.BILINEAR`` -``Image.LINEAR`` ``Image.Resampling.BILINEAR`` -``Image.HAMMING`` ``Image.Resampling.HAMMING`` -``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` -``Image.CUBIC`` ``Image.Resampling.BICUBIC`` -``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` -``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` -``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` -``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` -``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` -``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` ``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` ``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` ``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a760de575..a31ec3800 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -65,21 +65,16 @@ def __getattr__(name): if name in categories: deprecate("Image categories", 10, "is_animated", plural=True) return categories[name] - elif name in ("NEAREST", "NONE"): - deprecate(name, 10, "Resampling.NEAREST or Dither.NONE") - return 0 old_resampling = { "LINEAR": "BILINEAR", "CUBIC": "BICUBIC", "ANTIALIAS": "LANCZOS", } if name in old_resampling: - deprecate(name, 10, f"Resampling.{old_resampling[name]}") + deprecate( + name, 10, f"{old_resampling[name]} or Resampling.{old_resampling[name]}" + ) return Resampling[old_resampling[name]] - for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): - if name in enum.__members__: - deprecate(name, 10, f"{enum.__name__}.{name}") - return enum[name] raise AttributeError(f"module '{__name__}' has no attribute '{name}'") @@ -216,6 +211,12 @@ class Quantize(IntEnum): LIBIMAGEQUANT = 3 +module = sys.modules[__name__] +for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): + for item in enum: + setattr(module, item.name, item.value) + + if hasattr(core, "DEFAULT_STRATEGY"): DEFAULT_STRATEGY = core.DEFAULT_STRATEGY FILTERED = core.FILTERED From a9c46bc288d23c95fd08ee66493cb07be074f02e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 10:22:10 +1100 Subject: [PATCH 015/220] Document "transparency" info key --- docs/handbook/concepts.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index f3fa1f2b1..f7bc9396b 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -111,6 +111,18 @@ the file format handler (see the chapter on :ref:`image-file-formats`). Most handlers add properties to the :py:attr:`~PIL.Image.Image.info` attribute when loading an image, but ignore it when saving images. +Transparency +------------ + +If an image does not have an alpha band, transparency may be specified in the +:py:attr:`~PIL.Image.Image.info` attribute with a "transparency" key. + +Most of the time, the "transparency" value is a single integer, describing +which pixel value is transparent in an "1", "L", "I" or "P" mode image. +However, PNG images may have three values, one for each channel in an "RGB" +mode image, or can have a byte string for a "P" mode image, to specify the +alpha value for each palette entry. + Orientation ----------- From 0da8e43977f11837d9175419884d6a3295a7651e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 08:58:38 +1100 Subject: [PATCH 016/220] Parametrized test --- Tests/test_file_tiff_metadata.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 48c0273fe..48797ea08 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -185,21 +185,21 @@ def test_iptc(tmp_path): im.save(out) -def test_writing_other_types_to_ascii(tmp_path): - im = hopper() +@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) +def test_writing_other_types_to_ascii(value, expected, tmp_path): info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] assert tag.type == TiffTags.ASCII + info[271] = value + + im = hopper() out = str(tmp_path / "temp.tiff") - for (value, expected) in {b"test": "test", 1: "1"}.items(): - info[271] = value + im.save(out, tiffinfo=info) - im.save(out, tiffinfo=info) - - with Image.open(out) as reloaded: - assert reloaded.tag_v2[271] == expected + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == expected def test_writing_int_to_bytes(tmp_path): From cd351c4f854b6fffde086ec43c1149f2dbcba472 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 09:41:14 +1100 Subject: [PATCH 017/220] Added release notes --- docs/releasenotes/9.4.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index aae3e2b64..e4e1e40fe 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -45,6 +45,12 @@ removes the hidden RGB values for better compression by default in libwebp 0.5 or later. By setting this option to ``True``, the encoder will keep the hidden RGB values. +Added ``signed`` option when saving JPEG2000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the ``signed`` keyword argument is present and true when saving JPEG2000 +images, then tell the encoder to save the image as signed. + Added IFD, Interop and LightSource ExifTags enums ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 73a2c3049f905bba20748c82ce12e6ca971360f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 10:27:03 +1100 Subject: [PATCH 018/220] Use pytest.raises match argument --- Tests/test_imagegrab.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 01442dc69..317db4c01 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -65,12 +65,12 @@ $bmp = New-Object Drawing.Bitmap 200, 200 p.communicate() else: if not shutil.which("wl-paste"): - with pytest.raises(NotImplementedError) as e: + with pytest.raises( + NotImplementedError, + match="wl-paste or xclip is required for" + r" ImageGrab.grabclipboard\(\) on Linux", + ): ImageGrab.grabclipboard() - assert ( - str(e.value) == "wl-paste or xclip is required" - " for ImageGrab.grabclipboard() on Linux" - ) return ImageGrab.grabclipboard() From a4baeda9f69a7ada9b78437be10adb66c3520b75 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 29 Dec 2022 11:07:16 +1100 Subject: [PATCH 019/220] Fixed typo Co-authored-by: Hugo van Kemenade --- docs/handbook/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index f7bc9396b..ed25e1865 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -118,7 +118,7 @@ If an image does not have an alpha band, transparency may be specified in the :py:attr:`~PIL.Image.Image.info` attribute with a "transparency" key. Most of the time, the "transparency" value is a single integer, describing -which pixel value is transparent in an "1", "L", "I" or "P" mode image. +which pixel value is transparent in a "1", "L", "I" or "P" mode image. However, PNG images may have three values, one for each channel in an "RGB" mode image, or can have a byte string for a "P" mode image, to specify the alpha value for each palette entry. From dc30ccc6b20d7234e0e3a1e5ba29bf80fa61b56e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 12:05:04 +1100 Subject: [PATCH 020/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 76fc230a8..cc6bb2e3e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Patch OpenJPEG to include ARM64 fix #6718 + [radarhere] + +- Added support for I;16 modes in putdata() #6825 + [radarhere] + +- Added conversion from RGBa to RGB #6708 + [radarhere] + - Added DDS support for uncompressed L and LA images #6820 [radarhere, REDxEYE] From efa27a70d634e0c9f65f71f3f8fcd9d748ded5c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 13:18:45 +1100 Subject: [PATCH 021/220] Document the meaning of "premultiplied alpha" --- docs/handbook/concepts.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index ed25e1865..01f75e9a3 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -64,6 +64,12 @@ Pillow also provides limited support for a few additional modes, including: * ``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)`` +would convert to an RGBa pixel of ``(5, 10, 15, 127)``. The values of the R, +G and B channels are halved as a result of the half transparency in the alpha +channel. + Apart from these additional modes, Pillow doesn't yet support multichannel images with a depth of more than 8 bits per channel. From 21e811117e3dfa0e1a93c54e239748ec2d221fe8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 13:55:13 +1100 Subject: [PATCH 022/220] Updated release notes --- docs/deprecations.rst | 5 +++++ docs/releasenotes/9.1.0.rst | 5 +++++ docs/releasenotes/9.4.0.rst | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index bbd873800..a84a9fe36 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -74,6 +74,11 @@ Constants A number of constants have been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +.. note:: + + Additional ``Image`` constants were deprecated in Pillow 9.1.0, but they + were later restored in Pillow 9.4.0. See :ref:`restored-image-constants` + ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 48ce6fef7..e97b58a41 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -53,6 +53,11 @@ Constants A number of constants have been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +.. note:: + + Some of these deprecations were restored in Pillow 9.4.0. See + :ref:`restored-image-constants` + ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index aae3e2b64..e4c47401c 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -103,3 +103,40 @@ Added support for DDS L and LA images Support has been added to read and write L and LA DDS images in the uncompressed format, known as "luminance" textures. + +.. _restored-image-constants: + +Constants +^^^^^^^^^ + +In Pillow 9.1.0, the following constants were deprecated. Those deprecations have now +been restored. + +- ``Image.NONE`` +- ``Image.NEAREST`` +- ``Image.ORDERED`` +- ``Image.RASTERIZE`` +- ``Image.FLOYDSTEINBERG`` +- ``Image.WEB`` +- ``Image.ADAPTIVE`` +- ``Image.AFFINE`` +- ``Image.EXTENT`` +- ``Image.PERSPECTIVE`` +- ``Image.QUAD`` +- ``Image.MESH`` +- ``Image.FLIP_LEFT_RIGHT`` +- ``Image.FLIP_TOP_BOTTOM`` +- ``Image.ROTATE_90`` +- ``Image.ROTATE_180`` +- ``Image.ROTATE_270`` +- ``Image.TRANSPOSE`` +- ``Image.TRANSVERSE`` +- ``Image.BOX`` +- ``Image.BILINEAR`` +- ``Image.HAMMING`` +- ``Image.BICUBIC`` +- ``Image.LANCZOS`` +- ``Image.MEDIANCUT`` +- ``Image.MAXCOVERAGE`` +- ``Image.FASTOCTREE`` +- ``Image.LIBIMAGEQUANT`` From a7f8e862cb1310fb093247ff69085efdef51967e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 21:08:58 +1100 Subject: [PATCH 023/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cc6bb2e3e..aa0fa2a74 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- If available, use wl-paste or xclip for grabclipboard() on Linux #6783 + [radarhere] + +- Added signed option when saving JPEG2000 images #6709 + [radarhere] + - Patch OpenJPEG to include ARM64 fix #6718 [radarhere] From 1e3f3ab5963aca613e27c8d2d46f68c89fc78a09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 21:52:09 +1100 Subject: [PATCH 024/220] Do not attempt to read IFD1 if absent --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0a79b1237..f7b1ebd9f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3687,7 +3687,7 @@ class Exif(MutableMapping): def get_ifd(self, tag): if tag not in self._ifds: if tag == ExifTags.IFD.IFD1: - if self._info is not None: + if self._info is not None and self._info.next != 0: self._ifds[tag] = self._get_ifd_dict(self._info.next) elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: offset = self._hidden_data.get(tag, self.get(tag)) From 3a1f4b4919726c1c8a0ec4fbea1a908c41a0491f Mon Sep 17 00:00:00 2001 From: smb123w64gb Date: Thu, 29 Dec 2022 06:16:49 -0800 Subject: [PATCH 025/220] Fix version mismatch --- 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 6ded944da..a061aaf17 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -289,7 +289,7 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.14.tar.gz/download", + "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", "filename": "lcms2-2.14.tar.gz", "dir": "lcms2-2.14", "license": "COPYING", From 77f6f54ac46f9caa5d5063cbbeda0cddb6235bfc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Dec 2022 08:57:36 +1100 Subject: [PATCH 026/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa0fa2a74..4eebbda6a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Do not attempt to read IFD1 if absent #6840 + [radarhere] + +- Fixed writing int as ASCII tag #6800 + [radarhere] + - If available, use wl-paste or xclip for grabclipboard() on Linux #6783 [radarhere] From 2ae55ccbdad9c842929fb238ea1eb81d1f999024 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 21 Dec 2022 23:51:35 +0200 Subject: [PATCH 027/220] Improve exception traceback readability --- .pre-commit-config.yaml | 3 +- docs/example/DdsImagePlugin.py | 18 +- .../writing-your-own-image-plugin.rst | 3 +- setup.py | 18 +- src/PIL/BdfFontFile.py | 3 +- src/PIL/BlpImagePlugin.py | 35 ++-- src/PIL/BmpImagePlugin.py | 27 ++- src/PIL/BufrStubImagePlugin.py | 6 +- src/PIL/CurImagePlugin.py | 6 +- src/PIL/DcxImagePlugin.py | 3 +- src/PIL/DdsImagePlugin.py | 20 ++- src/PIL/EpsImagePlugin.py | 24 ++- src/PIL/FitsImagePlugin.py | 9 +- src/PIL/FitsStubImagePlugin.py | 3 +- src/PIL/FliImagePlugin.py | 6 +- src/PIL/FpxImagePlugin.py | 15 +- src/PIL/FtexImagePlugin.py | 9 +- src/PIL/GbrImagePlugin.py | 15 +- src/PIL/GdImageFile.py | 9 +- src/PIL/GifImagePlugin.py | 9 +- src/PIL/GimpGradientFile.py | 6 +- src/PIL/GimpPaletteFile.py | 9 +- src/PIL/GribStubImagePlugin.py | 6 +- src/PIL/Hdf5StubImagePlugin.py | 6 +- src/PIL/IcnsImagePlugin.py | 24 ++- src/PIL/IcoImagePlugin.py | 6 +- src/PIL/ImImagePlugin.py | 18 +- src/PIL/Image.py | 154 ++++++++++++------ src/PIL/ImageCms.py | 29 ++-- src/PIL/ImageColor.py | 6 +- src/PIL/ImageDraw.py | 65 +++++--- src/PIL/ImageFile.py | 51 ++++-- src/PIL/ImageFilter.py | 34 ++-- src/PIL/ImageFont.py | 32 ++-- src/PIL/ImageGrab.py | 8 +- src/PIL/ImageMath.py | 12 +- src/PIL/ImageMorph.py | 24 ++- src/PIL/ImageOps.py | 9 +- src/PIL/ImagePalette.py | 26 +-- src/PIL/ImageQt.py | 3 +- src/PIL/ImageSequence.py | 3 +- src/PIL/ImageShow.py | 21 ++- src/PIL/ImageStat.py | 3 +- src/PIL/ImageTk.py | 3 +- src/PIL/ImtImagePlugin.py | 3 +- src/PIL/IptcImagePlugin.py | 9 +- src/PIL/Jpeg2KImagePlugin.py | 23 ++- src/PIL/JpegImagePlugin.py | 54 ++++-- src/PIL/McIdasImagePlugin.py | 6 +- src/PIL/MicImagePlugin.py | 9 +- src/PIL/MpegImagePlugin.py | 3 +- src/PIL/MpoImagePlugin.py | 3 +- src/PIL/MspImagePlugin.py | 20 ++- src/PIL/PaletteFile.py | 3 +- src/PIL/PalmImagePlugin.py | 6 +- src/PIL/PcdImagePlugin.py | 3 +- src/PIL/PcfFontFile.py | 6 +- src/PIL/PcxImagePlugin.py | 12 +- src/PIL/PdfImagePlugin.py | 6 +- src/PIL/PdfParser.py | 14 +- src/PIL/PixarImagePlugin.py | 3 +- src/PIL/PngImagePlugin.py | 85 ++++++---- src/PIL/PpmImagePlugin.py | 33 ++-- src/PIL/PsdImagePlugin.py | 12 +- src/PIL/PyAccess.py | 6 +- src/PIL/SgiImagePlugin.py | 17 +- src/PIL/SpiderImagePlugin.py | 18 +- src/PIL/SunImagePlugin.py | 15 +- src/PIL/TarIO.py | 6 +- src/PIL/TgaImagePlugin.py | 12 +- src/PIL/TiffImagePlugin.py | 83 ++++++---- src/PIL/WebPImagePlugin.py | 14 +- src/PIL/WmfImagePlugin.py | 9 +- src/PIL/XVThumbImagePlugin.py | 6 +- src/PIL/XbmImagePlugin.py | 6 +- src/PIL/XpmImagePlugin.py | 15 +- src/PIL/_deprecate.py | 9 +- src/PIL/features.py | 9 +- winbuild/build_prepare.py | 11 +- 79 files changed, 861 insertions(+), 487 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 609352f22..d019d3e7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,8 @@ repos: rev: 6.0.0 hooks: - id: flake8 - additional_dependencies: [flake8-2020, flake8-implicit-str-concat] + additional_dependencies: + [flake8-2020, flake8-errmsg, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.9.0 diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index ec3938b36..26451533e 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -211,13 +211,16 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not a DDS file") + msg = "not a DDS file" + raise SyntaxError(msg) (header_size,) = struct.unpack("= 16 @@ -164,7 +165,8 @@ class BmpImageFile(ImageFile.ImageFile): # ---------------------- Check bit depth for unusual unsupported values self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) if self.mode is None: - raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})") + msg = f"Unsupported BMP pixel depth ({file_info['bits']})" + raise OSError(msg) # ---------------- Process BMP with Bitfields compression (not palette) decoder_name = "raw" @@ -205,23 +207,27 @@ class BmpImageFile(ImageFile.ImageFile): ): raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] else: - raise OSError("Unsupported BMP bitfields layout") + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) else: - raise OSError("Unsupported BMP bitfields layout") + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) elif file_info["compression"] == self.RAW: if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset raw_mode, self.mode = "BGRA", "RGBA" elif file_info["compression"] in (self.RLE8, self.RLE4): decoder_name = "bmp_rle" else: - raise OSError(f"Unsupported BMP compression ({file_info['compression']})") + msg = f"Unsupported BMP compression ({file_info['compression']})" + raise OSError(msg) # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): - raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})") + msg = f"Unsupported BMP Palette size ({file_info['colors']})" + raise OSError(msg) else: padding = file_info["palette_padding"] palette = read(padding * file_info["colors"]) @@ -271,7 +277,8 @@ class BmpImageFile(ImageFile.ImageFile): head_data = self.fp.read(14) # choke if the file does not have the required magic bytes if not _accept(head_data): - raise SyntaxError("Not a BMP file") + msg = "Not a BMP file" + raise SyntaxError(msg) # read the start position of the BMP image data (u32) offset = i32(head_data, 10) # load bitmap information (offset=raster info) @@ -383,7 +390,8 @@ def _save(im, fp, filename, bitmap_header=True): try: rawmode, bits, colors = SAVE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as BMP") from e + msg = f"cannot write mode {im.mode} as BMP" + raise OSError(msg) from e info = im.encoderinfo @@ -411,7 +419,8 @@ def _save(im, fp, filename, bitmap_header=True): offset = 14 + header + colors * 4 file_size = offset + image if file_size > 2**32 - 1: - raise ValueError("File size is too large for the BMP format") + msg = "File size is too large for the BMP format" + raise ValueError(msg) fp.write( b"BM" # file type (magic) + o32(file_size) # file size diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 9510f733e..a0da1b786 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -42,7 +42,8 @@ class BufrStubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(4)): - raise SyntaxError("Not a BUFR file") + msg = "Not a BUFR file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class BufrStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("BUFR save handler not installed") + msg = "BUFR save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 42af5cafc..aedc6ce7f 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -43,7 +43,8 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # check magic s = self.fp.read(6) if not _accept(s): - raise SyntaxError("not a CUR file") + msg = "not a CUR file" + raise SyntaxError(msg) # pick the largest cursor in the file m = b"" @@ -54,7 +55,8 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): elif s[0] > m[0] and s[1] > m[1]: m = s if not m: - raise TypeError("No cursors were found") + msg = "No cursors were found" + raise TypeError(msg) # load as bitmap self._bitmap(i32(m, 12) + offset) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index aeed1e7c7..81c0314f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -47,7 +47,8 @@ class DcxImageFile(PcxImageFile): # Header s = self.fp.read(4) if not _accept(s): - raise SyntaxError("not a DCX file") + msg = "not a DCX file" + raise SyntaxError(msg) # Component directory self._offset = [] diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index f78c8b17c..a946daeaa 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -114,13 +114,16 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not a DDS file") + msg = "not a DDS file" + raise SyntaxError(msg) (header_size,) = struct.unpack(" 255: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) try: m = split.match(s) except re.error as e: - raise SyntaxError("not an EPS file") from e + msg = "not an EPS file" + raise SyntaxError(msg) from e if m: k, v = m.group(1, 2) @@ -268,7 +271,8 @@ class EpsImageFile(ImageFile.ImageFile): # tools mistakenly put in the Comments section pass else: - raise OSError("bad EPS header") + msg = "bad EPS header" + raise OSError(msg) s_raw = fp.readline() s = s_raw.strip("\r\n") @@ -282,7 +286,8 @@ class EpsImageFile(ImageFile.ImageFile): while s[:1] == "%": if len(s) > 255: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) if s[:11] == "%ImageData:": # Encoded bitmapped image. @@ -306,7 +311,8 @@ class EpsImageFile(ImageFile.ImageFile): break if not box: - raise OSError("cannot determine EPS bounding box") + msg = "cannot determine EPS bounding box" + raise OSError(msg) def _find_offset(self, fp): @@ -326,7 +332,8 @@ class EpsImageFile(ImageFile.ImageFile): offset = i32(s, 4) length = i32(s, 8) else: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) return length, offset @@ -365,7 +372,8 @@ def _save(im, fp, filename, eps=1): elif im.mode == "CMYK": operator = (8, 4, b"false 4 colorimage") else: - raise ValueError("image mode is not supported") + msg = "image mode is not supported" + raise ValueError(msg) if eps: # diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index c16300efa..536bc1fe6 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -28,7 +28,8 @@ class FitsImageFile(ImageFile.ImageFile): while True: header = self.fp.read(80) if not header: - raise OSError("Truncated FITS file") + msg = "Truncated FITS file" + raise OSError(msg) keyword = header[:8].strip() if keyword == b"END": break @@ -36,12 +37,14 @@ class FitsImageFile(ImageFile.ImageFile): if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): - raise SyntaxError("Not a FITS file") + msg = "Not a FITS file" + raise SyntaxError(msg) headers[keyword] = value naxis = int(headers[b"NAXIS"]) if naxis == 0: - raise ValueError("No image data") + msg = "No image data" + raise ValueError(msg) elif naxis == 1: self._size = 1, int(headers[b"NAXIS1"]) else: diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 440240a99..86eb2d5a2 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -67,7 +67,8 @@ class FITSStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): - raise OSError("FITS save handler not installed") + msg = "FITS save handler not installed" + raise OSError(msg) # -------------------------------------------------------------------- diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 908bed9f4..66681939d 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -50,7 +50,8 @@ class FliImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): - raise SyntaxError("not an FLI/FLC file") + msg = "not an FLI/FLC file" + raise SyntaxError(msg) # frames self.n_frames = i16(s, 6) @@ -141,7 +142,8 @@ class FliImageFile(ImageFile.ImageFile): self.load() if frame != self.__frame + 1: - raise ValueError(f"cannot seek to frame {frame}") + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) self.__frame = frame # move to next frame diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index a55376d0e..8ddc6b40b 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -60,10 +60,12 @@ class FpxImageFile(ImageFile.ImageFile): try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: - raise SyntaxError("not an FPX file; invalid OLE file") from e + msg = "not an FPX file; invalid OLE file" + raise SyntaxError(msg) from e if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": - raise SyntaxError("not an FPX file; bad root CLSID") + msg = "not an FPX file; bad root CLSID" + raise SyntaxError(msg) self._open_index(1) @@ -99,7 +101,8 @@ class FpxImageFile(ImageFile.ImageFile): colors = [] bands = i32(s, 4) if bands > 4: - raise OSError("Invalid number of bands") + msg = "Invalid number of bands" + raise OSError(msg) for i in range(bands): # note: for now, we ignore the "uncalibrated" flag colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) @@ -141,7 +144,8 @@ class FpxImageFile(ImageFile.ImageFile): length = i32(s, 32) if size != self.size: - raise OSError("subimage mismatch") + msg = "subimage mismatch" + raise OSError(msg) # get tile descriptors fp.seek(28 + offset) @@ -217,7 +221,8 @@ class FpxImageFile(ImageFile.ImageFile): self.tile_prefix = self.jpeg[jpeg_tables] else: - raise OSError("unknown/invalid compression") + msg = "unknown/invalid compression" + raise OSError(msg) x = x + xtile if x >= xsize: diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 1b714eb4f..c7c32252b 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -73,7 +73,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) class FtexImageFile(ImageFile.ImageFile): @@ -82,7 +83,8 @@ class FtexImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not an FTEX file") + msg = "not an FTEX file" + raise SyntaxError(msg) struct.unpack(" 100: - raise SyntaxError("bad palette file") + msg = "bad palette file" + raise SyntaxError(msg) v = tuple(map(int, s.split()[:3])) if len(v) != 3: - raise ValueError("bad palette entry") + msg = "bad palette entry" + raise ValueError(msg) self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 4575f8237..2088eb7b0 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -42,7 +42,8 @@ class GribStubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(8)): - raise SyntaxError("Not a GRIB file") + msg = "Not a GRIB file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class GribStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("GRIB save handler not installed") + msg = "GRIB save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index df11cf2a6..d6f283739 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -42,7 +42,8 @@ class HDF5StubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(8)): - raise SyntaxError("Not an HDF file") + msg = "Not an HDF file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class HDF5StubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("HDF5 save handler not installed") + msg = "HDF5 save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index fa192f053..e76d0c35a 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -42,7 +42,8 @@ def read_32t(fobj, start_length, size): fobj.seek(start) sig = fobj.read(4) if sig != b"\x00\x00\x00\x00": - raise SyntaxError("Unknown signature, expecting 0x00000000") + msg = "Unknown signature, expecting 0x00000000" + raise SyntaxError(msg) return read_32(fobj, (start + 4, length - 4), size) @@ -82,7 +83,8 @@ def read_32(fobj, start_length, size): if bytesleft <= 0: break if bytesleft != 0: - raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]") + msg = f"Error reading channel [{repr(bytesleft)} left]" + raise SyntaxError(msg) band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) im.im.putband(band.im, band_ix) return {"RGB": im} @@ -113,10 +115,11 @@ def read_png_or_jpeg2000(fobj, start_length, size): or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" ): if not enable_jpeg2k: - raise ValueError( + msg = ( "Unsupported icon subimage format (rebuild PIL " "with JPEG 2000 support to fix this)" ) + raise ValueError(msg) # j2k, jpc or j2c fobj.seek(start) jp2kstream = fobj.read(length) @@ -127,7 +130,8 @@ def read_png_or_jpeg2000(fobj, start_length, size): im = im.convert("RGBA") return {"RGBA": im} else: - raise ValueError("Unsupported icon subimage format") + msg = "Unsupported icon subimage format" + raise ValueError(msg) class IcnsFile: @@ -168,12 +172,14 @@ class IcnsFile: self.fobj = fobj sig, filesize = nextheader(fobj) if not _accept(sig): - raise SyntaxError("not an icns file") + msg = "not an icns file" + raise SyntaxError(msg) i = HEADERSIZE while i < filesize: sig, blocksize = nextheader(fobj) if blocksize <= 0: - raise SyntaxError("invalid block header") + msg = "invalid block header" + raise SyntaxError(msg) i += HEADERSIZE blocksize -= HEADERSIZE dct[sig] = (i, blocksize) @@ -192,7 +198,8 @@ class IcnsFile: def bestsize(self): sizes = self.itersizes() if not sizes: - raise SyntaxError("No 32bit icon resources found") + msg = "No 32bit icon resources found" + raise SyntaxError(msg) return max(sizes) def dataforsize(self, size): @@ -275,7 +282,8 @@ class IcnsImageFile(ImageFile.ImageFile): if value in simple_sizes: info_size = self.info["sizes"][simple_sizes.index(value)] if info_size not in self.info["sizes"]: - raise ValueError("This is not one of the allowed sizes of this image") + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) self._size = value def load(self): diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 93b9dfdea..568e6d38d 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -127,7 +127,8 @@ class IcoFile: # check magic s = buf.read(6) if not _accept(s): - raise SyntaxError("not an ICO file") + msg = "not an ICO file" + raise SyntaxError(msg) self.buf = buf self.entry = [] @@ -316,7 +317,8 @@ class IcoImageFile(ImageFile.ImageFile): @size.setter def size(self, value): if value not in self.info["sizes"]: - raise ValueError("This is not one of the allowed sizes of this image") + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) self._size = value def load(self): diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 31b0ff469..d0e9508fe 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -126,7 +126,8 @@ class ImImageFile(ImageFile.ImageFile): # 100 bytes, this is (probably) not a text header. if b"\n" not in self.fp.read(100): - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) self.fp.seek(0) n = 0 @@ -153,7 +154,8 @@ class ImImageFile(ImageFile.ImageFile): s = s + self.fp.readline() if len(s) > 100: - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) if s[-2:] == b"\r\n": s = s[:-2] @@ -163,7 +165,8 @@ class ImImageFile(ImageFile.ImageFile): try: m = split.match(s) except re.error as e: - raise SyntaxError("not an IM file") from e + msg = "not an IM file" + raise SyntaxError(msg) from e if m: @@ -203,7 +206,8 @@ class ImImageFile(ImageFile.ImageFile): ) if not n: - raise SyntaxError("Not an IM file") + msg = "Not an IM file" + raise SyntaxError(msg) # Basic attributes self._size = self.info[SIZE] @@ -213,7 +217,8 @@ class ImImageFile(ImageFile.ImageFile): while s and s[:1] != b"\x1A": s = self.fp.read(1) if not s: - raise SyntaxError("File truncated") + msg = "File truncated" + raise SyntaxError(msg) if LUT in self.info: # convert lookup table to palette or lut attribute @@ -332,7 +337,8 @@ def _save(im, fp, filename): try: image_type, rawmode = SAVE[im.mode] except KeyError as e: - raise ValueError(f"Cannot save {im.mode} images as IM") from e + msg = f"Cannot save {im.mode} images as IM" + raise ValueError(msg) from e frames = im.encoderinfo.get("frames", 1) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f7b1ebd9f..386fb7c26 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -80,7 +80,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(name, 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) logger = logging.getLogger(__name__) @@ -107,11 +108,12 @@ try: from . import _imaging as core if __version__ != getattr(core, "PILLOW_VERSION", None): - raise ImportError( + msg = ( "The _imaging extension was built for another version of Pillow or PIL:\n" f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" f"Pillow version: {__version__}" ) + raise ImportError(msg) except ImportError as v: core = DeferredError(ImportError("The _imaging C module is not installed.")) @@ -406,7 +408,8 @@ def _getdecoder(mode, decoder_name, args, extra=()): # get decoder decoder = getattr(core, decoder_name + "_decoder") except AttributeError as e: - raise OSError(f"decoder {decoder_name} not available") from e + msg = f"decoder {decoder_name} not available" + raise OSError(msg) from e return decoder(mode, *args + extra) @@ -429,7 +432,8 @@ def _getencoder(mode, encoder_name, args, extra=()): # get encoder encoder = getattr(core, encoder_name + "_encoder") except AttributeError as e: - raise OSError(f"encoder {encoder_name} not available") from e + msg = f"encoder {encoder_name} not available" + raise OSError(msg) from e return encoder(mode, *args + extra) @@ -675,7 +679,8 @@ class Image: try: self.save(b, "PNG") except Exception as e: - raise ValueError("Could not save to PNG for display") from e + msg = "Could not save to PNG for display" + raise ValueError(msg) from e return b.getvalue() @property @@ -767,7 +772,8 @@ class Image: if s: break if s < 0: - raise RuntimeError(f"encoder error {s} in tobytes") + msg = f"encoder error {s} in tobytes" + raise RuntimeError(msg) return b"".join(data) @@ -784,7 +790,8 @@ class Image: self.load() if self.mode != "1": - raise ValueError("not a bitmap") + msg = "not a bitmap" + raise ValueError(msg) data = self.tobytes("xbm") return b"".join( [ @@ -818,9 +825,11 @@ class Image: s = d.decode(data) if s[0] >= 0: - raise ValueError("not enough image data") + msg = "not enough image data" + raise ValueError(msg) if s[1] != 0: - raise ValueError("cannot decode image data") + msg = "cannot decode image data" + raise ValueError(msg) def load(self): """ @@ -941,7 +950,8 @@ class Image: if matrix: # matrix conversion if mode not in ("L", "RGB"): - raise ValueError("illegal conversion") + msg = "illegal conversion" + raise ValueError(msg) im = self.im.convert_matrix(mode, matrix) new = self._new(im) if has_transparency and self.im.bands == 3: @@ -1026,7 +1036,8 @@ class Image: elif isinstance(t, int): self.im.putpalettealpha(t, 0) else: - raise ValueError("Transparency for P mode should be bytes or int") + msg = "Transparency for P mode should be bytes or int" + raise ValueError(msg) if mode == "P" and palette == Palette.ADAPTIVE: im = self.im.quantize(colors) @@ -1076,7 +1087,8 @@ class Image: im = self.im.convert(modebase) im = im.convert(mode, dither) except KeyError as e: - raise ValueError("illegal conversion") from e + msg = "illegal conversion" + raise ValueError(msg) from e new_im = self._new(im) if mode == "P" and palette != Palette.ADAPTIVE: @@ -1151,20 +1163,21 @@ class Image: Quantize.LIBIMAGEQUANT, ): # Caller specified an invalid mode. - raise ValueError( + msg = ( "Fast Octree (method == 2) and libimagequant (method == 3) " "are the only valid methods for quantizing RGBA images" ) + raise ValueError(msg) if palette: # use palette from reference image palette.load() if palette.mode != "P": - raise ValueError("bad mode for palette image") + msg = "bad mode for palette image" + raise ValueError(msg) if self.mode != "RGB" and self.mode != "L": - raise ValueError( - "only RGB or L mode images can be quantized to a palette" - ) + msg = "only RGB or L mode images can be quantized to a palette" + raise ValueError(msg) im = self.im.convert("P", dither, palette.im) new_im = self._new(im) new_im.palette = palette.palette.copy() @@ -1210,9 +1223,11 @@ class Image: return self.copy() if box[2] < box[0]: - raise ValueError("Coordinate 'right' is less than 'left'") + msg = "Coordinate 'right' is less than 'left'" + raise ValueError(msg) elif box[3] < box[1]: - raise ValueError("Coordinate 'lower' is less than 'upper'") + msg = "Coordinate 'lower' is less than 'upper'" + raise ValueError(msg) self.load() return self._new(self._crop(self.im, box)) @@ -1280,9 +1295,8 @@ class Image: if isinstance(filter, Callable): filter = filter() if not hasattr(filter, "filter"): - raise TypeError( - "filter argument should be ImageFilter.Filter instance or class" - ) + msg = "filter argument should be ImageFilter.Filter instance or class" + raise TypeError(msg) multiband = isinstance(filter, ImageFilter.MultibandFilter) if self.im.bands == 1 or multiband: @@ -1691,7 +1705,8 @@ class Image: size = mask.size else: # FIXME: use self.size here? - raise ValueError("cannot determine region size; use 4-item box") + msg = "cannot determine region size; use 4-item box" + raise ValueError(msg) box += (box[0] + size[0], box[1] + size[1]) if isinstance(im, str): @@ -1730,15 +1745,20 @@ class Image: """ if not isinstance(source, (list, tuple)): - raise ValueError("Source must be a tuple") + msg = "Source must be a tuple" + raise ValueError(msg) if not isinstance(dest, (list, tuple)): - raise ValueError("Destination must be a tuple") + msg = "Destination must be a tuple" + raise ValueError(msg) if not len(source) in (2, 4): - raise ValueError("Source must be a 2 or 4-tuple") + msg = "Source must be a 2 or 4-tuple" + raise ValueError(msg) if not len(dest) == 2: - raise ValueError("Destination must be a 2-tuple") + msg = "Destination must be a 2-tuple" + raise ValueError(msg) if min(source) < 0: - raise ValueError("Source must be non-negative") + msg = "Source must be non-negative" + raise ValueError(msg) if len(source) == 2: source = source + im.size @@ -1803,7 +1823,8 @@ class Image: if self.mode == "F": # FIXME: _imaging returns a confusing error message for this case - raise ValueError("point operation not supported for this mode") + msg = "point operation not supported for this mode" + raise ValueError(msg) if mode != "F": lut = [round(i) for i in lut] @@ -1837,7 +1858,8 @@ class Image: self.pyaccess = None self.mode = self.im.mode except KeyError as e: - raise ValueError("illegal image mode") from e + msg = "illegal image mode" + raise ValueError(msg) from e if self.mode in ("LA", "PA"): band = 1 @@ -1847,7 +1869,8 @@ class Image: if isImageType(alpha): # alpha layer if alpha.mode not in ("1", "L"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) alpha.load() if alpha.mode == "1": alpha = alpha.convert("L") @@ -1903,7 +1926,8 @@ class Image: from . import ImagePalette if self.mode not in ("L", "LA", "P", "PA"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) if isinstance(data, ImagePalette.ImagePalette): palette = ImagePalette.raw(data.rawmode, data.palette) else: @@ -1972,7 +1996,8 @@ class Image: from . import ImagePalette if self.mode not in ("L", "P"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) bands = 3 palette_mode = "RGB" @@ -2122,7 +2147,8 @@ class Image: ) if reducing_gap is not None and reducing_gap < 1.0: - raise ValueError("reducing_gap must be 1.0 or greater") + msg = "reducing_gap must be 1.0 or greater" + raise ValueError(msg) size = tuple(size) @@ -2380,7 +2406,8 @@ class Image: try: format = EXTENSION[ext] except KeyError as e: - raise ValueError(f"unknown file extension: {ext}") from e + msg = f"unknown file extension: {ext}" + raise ValueError(msg) from e if format.upper() not in SAVE: init() @@ -2494,7 +2521,8 @@ class Image: try: channel = self.getbands().index(channel) except ValueError as e: - raise ValueError(f'The image has no channel "{channel}"') from e + msg = f'The image has no channel "{channel}"' + raise ValueError(msg) from e return self._new(self.im.getband(channel)) @@ -2665,7 +2693,8 @@ class Image: method, data = method.getdata() if data is None: - raise ValueError("missing method data") + msg = "missing method data" + raise ValueError(msg) im = new(self.mode, size, fillcolor) if self.mode == "P" and self.palette: @@ -2726,7 +2755,8 @@ class Image: ) else: - raise ValueError("unknown transformation method") + msg = "unknown transformation method" + raise ValueError(msg) if resample not in ( Resampling.NEAREST, @@ -2791,7 +2821,8 @@ class Image: from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.toqimage(self) def toqpixmap(self): @@ -2799,7 +2830,8 @@ class Image: from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.toqpixmap(self) @@ -2847,11 +2879,14 @@ def _check_size(size): """ if not isinstance(size, (list, tuple)): - raise ValueError("Size must be a tuple") + msg = "Size must be a tuple" + raise ValueError(msg) if len(size) != 2: - raise ValueError("Size must be a tuple of length 2") + msg = "Size must be a tuple of length 2" + raise ValueError(msg) if size[0] < 0 or size[1] < 0: - raise ValueError("Width and height must be >= 0") + msg = "Width and height must be >= 0" + raise ValueError(msg) return True @@ -3037,7 +3072,8 @@ def fromarray(obj, mode=None): try: typekey = (1, 1) + shape[2:], arr["typestr"] except KeyError as e: - raise TypeError("Cannot handle this data type") from e + msg = "Cannot handle this data type" + raise TypeError(msg) from e try: mode, rawmode = _fromarray_typemap[typekey] except KeyError as e: @@ -3051,7 +3087,8 @@ def fromarray(obj, mode=None): else: ndmax = 4 if ndim > ndmax: - raise ValueError(f"Too many dimensions: {ndim} > {ndmax}.") + msg = f"Too many dimensions: {ndim} > {ndmax}." + raise ValueError(msg) size = 1 if ndim == 1 else shape[1], shape[0] if strides is not None: @@ -3068,7 +3105,8 @@ def fromqimage(im): from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.fromqimage(im) @@ -3077,7 +3115,8 @@ def fromqpixmap(im): from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.fromqpixmap(im) @@ -3115,10 +3154,11 @@ def _decompression_bomb_check(size): pixels = size[0] * size[1] if pixels > 2 * MAX_IMAGE_PIXELS: - raise DecompressionBombError( + msg = ( f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " "pixels, could be decompression bomb DOS attack." ) + raise DecompressionBombError(msg) if pixels > MAX_IMAGE_PIXELS: warnings.warn( @@ -3158,17 +3198,20 @@ def open(fp, mode="r", formats=None): """ if mode != "r": - raise ValueError(f"bad mode {repr(mode)}") + msg = f"bad mode {repr(mode)}" + raise ValueError(msg) elif isinstance(fp, io.StringIO): - raise ValueError( + msg = ( "StringIO cannot be used to open an image. " "Binary data must be used instead." ) + raise ValueError(msg) if formats is None: formats = ID elif not isinstance(formats, (list, tuple)): - raise TypeError("formats must be a list or tuple") + msg = "formats must be a list or tuple" + raise TypeError(msg) exclusive_fp = False filename = "" @@ -3326,12 +3369,15 @@ def merge(mode, bands): """ if getmodebands(mode) != len(bands) or "*" in mode: - raise ValueError("wrong number of bands") + msg = "wrong number of bands" + raise ValueError(msg) for band in bands[1:]: if band.mode != getmodetype(mode): - raise ValueError("mode mismatch") + msg = "mode mismatch" + raise ValueError(msg) if band.size != bands[0].size: - raise ValueError("size mismatch") + msg = "size mismatch" + raise ValueError(msg) for band in bands: band.load() return bands[0]._new(core.merge(mode, *[b.im for b in bands])) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 605252d5d..2a2d372e5 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -124,7 +124,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) # @@ -191,7 +192,8 @@ class ImageCmsProfile: elif isinstance(profile, _imagingcms.CmsProfile): self._set(profile) else: - raise TypeError("Invalid type for Profile") + msg = "Invalid type for Profile" + raise TypeError(msg) def _set(self, profile, filename=None): self.profile = profile @@ -269,7 +271,8 @@ class ImageCmsTransform(Image.ImagePointHandler): def apply_in_place(self, im): im.load() if im.mode != self.output_mode: - raise ValueError("mode mismatch") # wrong output mode + msg = "mode mismatch" + raise ValueError(msg) # wrong output mode self.transform.apply(im.im.id, im.im.id) im.info["icc_profile"] = self.output_profile.tobytes() return im @@ -374,10 +377,12 @@ def profileToProfile( outputMode = im.mode if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError(f"flags must be an integer between 0 and {_MAX_FLAG}") + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -489,7 +494,8 @@ def buildTransform( """ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) @@ -591,7 +597,8 @@ def buildProofTransform( """ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) @@ -705,17 +712,17 @@ def createProfile(colorSpace, colorTemp=-1): """ if colorSpace not in ["LAB", "XYZ", "sRGB"]: - raise PyCMSError( + msg = ( f"Color space not supported for on-the-fly profile creation ({colorSpace})" ) + raise PyCMSError(msg) if colorSpace == "LAB": try: colorTemp = float(colorTemp) except (TypeError, ValueError) as e: - raise PyCMSError( - f'Color temperature must be numeric, "{colorTemp}" not valid' - ) from e + msg = f'Color temperature must be numeric, "{colorTemp}" not valid' + raise PyCMSError(msg) from e try: return core.createProfile(colorSpace, colorTemp) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 9cbce4143..e184ed68d 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -33,7 +33,8 @@ def getrgb(color): :return: ``(red, green, blue[, alpha])`` """ if len(color) > 100: - raise ValueError("color specifier is too long") + msg = "color specifier is too long" + raise ValueError(msg) color = color.lower() rgb = colormap.get(color, None) @@ -115,7 +116,8 @@ def getrgb(color): m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) if m: return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) - raise ValueError(f"unknown color specifier: {repr(color)}") + msg = f"unknown color specifier: {repr(color)}" + raise ValueError(msg) def getcolor(color, mode): diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 407544234..ce29a163b 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -69,7 +69,8 @@ class ImageDraw: if mode == "RGBA" and im.mode == "RGB": blend = 1 else: - raise ValueError("mode mismatch") + msg = "mode mismatch" + raise ValueError(msg) if mode == "P": self.palette = im.palette else: @@ -437,7 +438,8 @@ class ImageDraw: ) if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if font is None: font = self.getfont() @@ -534,14 +536,17 @@ class ImageDraw: embedded_color=False, ): if direction == "ttb": - raise ValueError("ttb direction is unsupported for multiline text") + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) if anchor is None: anchor = "la" elif len(anchor) != 2: - raise ValueError("anchor must be a 2 character string") + msg = "anchor must be a 2 character string" + raise ValueError(msg) elif anchor[1] in "tb": - raise ValueError("anchor not supported for multiline text") + msg = "anchor not supported for multiline text" + raise ValueError(msg) widths = [] max_width = 0 @@ -578,7 +583,8 @@ class ImageDraw: elif align == "right": left += width_difference else: - raise ValueError('align must be "left", "center" or "right"') + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) self.text( (left, top), @@ -672,9 +678,11 @@ class ImageDraw: ): """Get the length of a given string, in pixels with 1/64 precision.""" if self._multiline_check(text): - raise ValueError("can't measure length of multiline text") + msg = "can't measure length of multiline text" + raise ValueError(msg) if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if font is None: font = self.getfont() @@ -712,7 +720,8 @@ class ImageDraw: ): """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if self._multiline_check(text): return self.multiline_textbbox( @@ -752,14 +761,17 @@ class ImageDraw: embedded_color=False, ): if direction == "ttb": - raise ValueError("ttb direction is unsupported for multiline text") + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) if anchor is None: anchor = "la" elif len(anchor) != 2: - raise ValueError("anchor must be a 2 character string") + msg = "anchor must be a 2 character string" + raise ValueError(msg) elif anchor[1] in "tb": - raise ValueError("anchor not supported for multiline text") + msg = "anchor not supported for multiline text" + raise ValueError(msg) widths = [] max_width = 0 @@ -803,7 +815,8 @@ class ImageDraw: elif align == "right": left += width_difference else: - raise ValueError('align must be "left", "center" or "right"') + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) bbox_line = self.textbbox( (left, top), @@ -979,38 +992,44 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 1. Error Handling # 1.1 Check `n_sides` has an appropriate value if not isinstance(n_sides, int): - raise TypeError("n_sides should be an int") + msg = "n_sides should be an int" + raise TypeError(msg) if n_sides < 3: - raise ValueError("n_sides should be an int > 2") + msg = "n_sides should be an int > 2" + raise ValueError(msg) # 1.2 Check `bounding_circle` has an appropriate value if not isinstance(bounding_circle, (list, tuple)): - raise TypeError("bounding_circle should be a tuple") + msg = "bounding_circle should be a tuple" + raise TypeError(msg) if len(bounding_circle) == 3: *centroid, polygon_radius = bounding_circle elif len(bounding_circle) == 2: centroid, polygon_radius = bounding_circle else: - raise ValueError( + msg = ( "bounding_circle should contain 2D coordinates " "and a radius (e.g. (x, y, r) or ((x, y), r) )" ) + raise ValueError(msg) if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): - raise ValueError("bounding_circle should only contain numeric data") + msg = "bounding_circle should only contain numeric data" + raise ValueError(msg) if not len(centroid) == 2: - raise ValueError( - "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" - ) + msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + raise ValueError(msg) if polygon_radius <= 0: - raise ValueError("bounding_circle radius should be > 0") + msg = "bounding_circle radius should be > 0" + raise ValueError(msg) # 1.3 Check `rotation` has an appropriate value if not isinstance(rotation, (int, float)): - raise ValueError("rotation should be an int or float") + msg = "rotation should be an int or float" + raise ValueError(msg) # 2. Define Helper Functions def _apply_rotation(point, degrees, centroid): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index dbdc0cb38..0d3facf57 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -124,7 +124,8 @@ class ImageFile(Image.Image): raise SyntaxError(v) from v if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: - raise SyntaxError("not identified by this driver") + msg = "not identified by this driver" + raise SyntaxError(msg) except BaseException: # close the file only if we have opened it this constructor if self._exclusive_fp: @@ -154,7 +155,8 @@ class ImageFile(Image.Image): """Load image data based on tile list""" if self.tile is None: - raise OSError("cannot load this image") + msg = "cannot load this image" + raise OSError(msg) pixel = Image.Image.load(self) if not self.tile: @@ -249,16 +251,18 @@ class ImageFile(Image.Image): if LOAD_TRUNCATED_IMAGES: break else: - raise OSError("image file is truncated") from e + msg = "image file is truncated" + raise OSError(msg) from e if not s: # truncated jpeg if LOAD_TRUNCATED_IMAGES: break else: - raise OSError( + msg = ( "image file is truncated " f"({len(b)} bytes not processed)" ) + raise OSError(msg) b = b + s n, err_code = decoder.decode(b) @@ -314,7 +318,8 @@ class ImageFile(Image.Image): and frame >= self.n_frames + self._min_frame ) ): - raise EOFError("attempt to seek outside sequence") + msg = "attempt to seek outside sequence" + raise EOFError(msg) return self.tell() != frame @@ -328,12 +333,14 @@ class StubImageFile(ImageFile): """ def _open(self): - raise NotImplementedError("StubImageFile subclass must implement _open") + msg = "StubImageFile subclass must implement _open" + raise NotImplementedError(msg) def load(self): loader = self._load() if loader is None: - raise OSError(f"cannot find loader for this {self.format} file") + msg = f"cannot find loader for this {self.format} file" + raise OSError(msg) image = loader.load(self) assert image is not None # become the other object (!) @@ -343,7 +350,8 @@ class StubImageFile(ImageFile): def _load(self): """(Hook) Find actual image loader.""" - raise NotImplementedError("StubImageFile subclass must implement _load") + msg = "StubImageFile subclass must implement _load" + raise NotImplementedError(msg) class Parser: @@ -468,9 +476,11 @@ class Parser: self.feed(b"") self.data = self.decoder = None if not self.finished: - raise OSError("image was incomplete") + msg = "image was incomplete" + raise OSError(msg) if not self.image: - raise OSError("cannot parse this image") + msg = "cannot parse this image" + raise OSError(msg) if self.data: # incremental parsing not possible; reopen the file # not that we have all data @@ -535,7 +545,8 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): # slight speedup: compress to real file object s = encoder.encode_to_file(fh, bufsize) if s < 0: - raise OSError(f"encoder error {s} when writing image file") from exc + msg = f"encoder error {s} when writing image file" + raise OSError(msg) from exc finally: encoder.cleanup() @@ -558,7 +569,8 @@ def _safe_read(fp, size): if size <= SAFEBLOCK: data = fp.read(size) if len(data) < size: - raise OSError("Truncated File Read") + msg = "Truncated File Read" + raise OSError(msg) return data data = [] remaining_size = size @@ -569,7 +581,8 @@ def _safe_read(fp, size): data.append(block) remaining_size -= len(block) if sum(len(d) for d in data) < size: - raise OSError("Truncated File Read") + msg = "Truncated File Read" + raise OSError(msg) return b"".join(data) @@ -645,13 +658,15 @@ class PyCodec: self.state.ysize = y1 - y0 if self.state.xsize <= 0 or self.state.ysize <= 0: - raise ValueError("Size cannot be negative") + msg = "Size cannot be negative" + raise ValueError(msg) if ( self.state.xsize + self.state.xoff > self.im.size[0] or self.state.ysize + self.state.yoff > self.im.size[1] ): - raise ValueError("Tile cannot extend outside image") + msg = "Tile cannot extend outside image" + raise ValueError(msg) class PyDecoder(PyCodec): @@ -696,9 +711,11 @@ class PyDecoder(PyCodec): s = d.decode(data) if s[0] >= 0: - raise ValueError("not enough image data") + msg = "not enough image data" + raise ValueError(msg) if s[1] != 0: - raise ValueError("cannot decode image data") + msg = "cannot decode image data" + raise ValueError(msg) class PyEncoder(PyCodec): diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index e10c6fdf1..59e2c18b9 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -28,7 +28,8 @@ class MultibandFilter(Filter): class BuiltinFilter(MultibandFilter): def filter(self, image): if image.mode == "P": - raise ValueError("cannot filter palette images") + msg = "cannot filter palette images" + raise ValueError(msg) return image.filter(*self.filterargs) @@ -57,7 +58,8 @@ class Kernel(BuiltinFilter): # default scale is sum of kernel scale = functools.reduce(lambda a, b: a + b, kernel) if size[0] * size[1] != len(kernel): - raise ValueError("not enough coefficients in kernel") + msg = "not enough coefficients in kernel" + raise ValueError(msg) self.filterargs = size, scale, offset, kernel @@ -80,7 +82,8 @@ class RankFilter(Filter): def filter(self, image): if image.mode == "P": - raise ValueError("cannot filter palette images") + msg = "cannot filter palette images" + raise ValueError(msg) image = image.expand(self.size // 2, self.size // 2) return image.rankfilter(self.size, self.rank) @@ -355,7 +358,8 @@ class Color3DLUT(MultibandFilter): def __init__(self, size, table, channels=3, target_mode=None, **kwargs): if channels not in (3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) self.size = size = self._check_size(size) self.channels = channels self.mode = target_mode @@ -395,19 +399,21 @@ class Color3DLUT(MultibandFilter): table, raw_table = [], table for pixel in raw_table: if len(pixel) != channels: - raise ValueError( + msg = ( "The elements of the table should " - "have a length of {}.".format(channels) + f"have a length of {channels}." ) + raise ValueError(msg) table.extend(pixel) if wrong_size or len(table) != items * channels: - raise ValueError( + msg = ( "The table should have either channels * size**3 float items " "or size**3 items of channels-sized tuples with floats. " f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " f"Actual length: {len(table)}" ) + raise ValueError(msg) self.table = table @staticmethod @@ -415,15 +421,15 @@ class Color3DLUT(MultibandFilter): try: _, _, _ = size except ValueError as e: - raise ValueError( - "Size should be either an integer or a tuple of three integers." - ) from e + msg = "Size should be either an integer or a tuple of three integers." + raise ValueError(msg) from e except TypeError: size = (size, size, size) size = [int(x) for x in size] for size_1d in size: if not 2 <= size_1d <= 65: - raise ValueError("Size should be in [2, 65] range.") + msg = "Size should be in [2, 65] range." + raise ValueError(msg) return size @classmethod @@ -441,7 +447,8 @@ class Color3DLUT(MultibandFilter): """ size_1d, size_2d, size_3d = cls._check_size(size) if channels not in (3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) table = [0] * (size_1d * size_2d * size_3d * channels) idx_out = 0 @@ -481,7 +488,8 @@ class Color3DLUT(MultibandFilter): lookup table. """ if channels not in (None, 3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) ch_in = self.channels ch_out = channels or ch_in size_1d, size_2d, size_3d = self.size diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 3b1a2a23a..b144c3dd2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -50,13 +50,15 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) class _ImagingFtNotInstalled: # module placeholder def __getattr__(self, id): - raise ImportError("The _imagingft C module is not installed") + msg = "The _imagingft C module is not installed" + raise ImportError(msg) try: @@ -105,7 +107,8 @@ class ImageFont: else: if image: image.close() - raise OSError("cannot find glyph data file") + msg = "cannot find glyph data file" + raise OSError(msg) self.file = fullname @@ -116,7 +119,8 @@ class ImageFont: # read PILfont header if file.readline() != b"PILfont\n": - raise SyntaxError("Not a PILfont file") + msg = "Not a PILfont file" + raise SyntaxError(msg) file.readline().split(b";") self.info = [] # FIXME: should be a dictionary while True: @@ -130,7 +134,8 @@ class ImageFont: # check image if image.mode not in ("1", "L"): - raise TypeError("invalid font image mode") + msg = "invalid font image mode" + raise TypeError(msg) image.load() @@ -817,7 +822,8 @@ class FreeTypeFont: try: names = self.font.getvarnames() except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e return [name.replace(b"\x00", b"") for name in names] def set_variation_by_name(self, name): @@ -847,7 +853,8 @@ class FreeTypeFont: try: axes = self.font.getvaraxes() except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e for axis in axes: axis["name"] = axis["name"].replace(b"\x00", b"") return axes @@ -860,7 +867,8 @@ class FreeTypeFont: try: self.font.setvaraxes(axes) except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e class TransposedFont: @@ -914,9 +922,8 @@ class TransposedFont: def getlength(self, text, *args, **kwargs): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - raise ValueError( - "text length is undefined for text rotated by 90 or 270 degrees" - ) + msg = "text length is undefined for text rotated by 90 or 270 degrees" + raise ValueError(msg) return self.font.getlength(text, *args, **kwargs) @@ -1061,7 +1068,8 @@ def load_path(filename): return load(os.path.join(directory, filename)) except OSError: pass - raise OSError("cannot find font file") + msg = "cannot find font file" + raise OSError(msg) def load_default(): diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 8cf956809..982f77f20 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -75,7 +75,8 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N return im # use xdisplay=None for default display on non-win32/macOS systems if not Image.core.HAVE_XCB: - raise OSError("Pillow was built without XCB support") + msg = "Pillow was built without XCB support" + raise OSError(msg) size, data = Image.core.grabscreen_x11(xdisplay) im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) if bbox: @@ -137,9 +138,8 @@ def grabclipboard(): elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: - raise NotImplementedError( - "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" - ) + msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" + raise NotImplementedError(msg) fh, filepath = tempfile.mkstemp() subprocess.call(args, stdout=fh) os.close(fh) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 09d9898d7..ac7d36b69 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -39,7 +39,8 @@ class _Operand: elif im1.im.mode in ("I", "F"): return im1.im else: - raise ValueError(f"unsupported mode: {im1.im.mode}") + msg = f"unsupported mode: {im1.im.mode}" + raise ValueError(msg) else: # argument was a constant if _isconstant(im1) and self.im.mode in ("1", "L", "I"): @@ -56,7 +57,8 @@ class _Operand: try: op = getattr(_imagingmath, op + "_" + im1.mode) except AttributeError as e: - raise TypeError(f"bad operand type for '{op}'") from e + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e _imagingmath.unop(op, out.im.id, im1.im.id) else: # binary operation @@ -80,7 +82,8 @@ class _Operand: try: op = getattr(_imagingmath, op + "_" + im1.mode) except AttributeError as e: - raise TypeError(f"bad operand type for '{op}'") from e + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) return _Operand(out) @@ -249,7 +252,8 @@ def eval(expression, _dict={}, **kw): for name in code.co_names: if name not in args and name != "abs": - raise ValueError(f"'{name}' not allowed") + msg = f"'{name}' not allowed" + raise ValueError(msg) scan(compiled_code) out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 1e22c36a8..60cbbedc3 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -81,7 +81,8 @@ class LutBuilder: ], } if op_name not in known_patterns: - raise Exception("Unknown pattern " + op_name + "!") + msg = "Unknown pattern " + op_name + "!" + raise Exception(msg) self.patterns = known_patterns[op_name] @@ -193,10 +194,12 @@ class MorphOp: Returns a tuple of the number of changed pixels and the morphed image""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage @@ -208,10 +211,12 @@ class MorphOp: Returns a list of tuples of (x,y) coordinates of all matching pixels. See :ref:`coordinate-system`.""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image): @@ -221,7 +226,8 @@ class MorphOp: of all matching pixels. See :ref:`coordinate-system`.""" if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename): @@ -231,12 +237,14 @@ class MorphOp: if len(self.lut) != LUT_SIZE: self.lut = None - raise Exception("Wrong size operator file!") + msg = "Wrong size operator file!" + raise Exception(msg) def save_lut(self, filename): """Save an operator to an mrl file""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) with open(filename, "wb") as f: f.write(self.lut) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 443c540b6..e2168ce62 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -49,13 +49,15 @@ def _color(color, mode): def _lut(image, lut): if image.mode == "P": # FIXME: apply to lookup table, not image data - raise NotImplementedError("mode P support coming soon") + msg = "mode P support coming soon" + raise NotImplementedError(msg) elif image.mode in ("L", "RGB"): if image.mode == "RGB" and len(lut) == 256: lut = lut + lut + lut return image.point(lut) else: - raise OSError("not supported for this image mode") + msg = "not supported for this image mode" + raise OSError(msg) # @@ -332,7 +334,8 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): if factor == 1: return image.copy() elif factor <= 0: - raise ValueError("the factor must be greater than 0") + msg = "the factor must be greater than 0" + raise ValueError(msg) else: size = (round(factor * image.width), round(factor * image.height)) return image.resize(size, resample) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fe76c86f4..fe0d32155 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -42,7 +42,8 @@ class ImagePalette: if size != 0: deprecate("The size parameter", 10, None) if size != len(self.palette): - raise ValueError("wrong palette size") + msg = "wrong palette size" + raise ValueError(msg) @property def palette(self): @@ -97,7 +98,8 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(self.palette, bytes): return self.palette arr = array.array("B", self.palette) @@ -112,14 +114,14 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(color, tuple): if self.mode == "RGB": if len(color) == 4: if color[3] != 255: - raise ValueError( - "cannot add non-opaque RGBA color to RGB palette" - ) + msg = "cannot add non-opaque RGBA color to RGB palette" + raise ValueError(msg) color = color[:3] elif self.mode == "RGBA": if len(color) == 3: @@ -147,7 +149,8 @@ class ImagePalette: index = i break if index >= 256: - raise ValueError("cannot allocate more than 256 colors") from e + msg = "cannot allocate more than 256 colors" + raise ValueError(msg) from e self.colors[color] = index if index * 3 < len(self.palette): self._palette = ( @@ -160,7 +163,8 @@ class ImagePalette: self.dirty = 1 return index else: - raise ValueError(f"unknown color specifier: {repr(color)}") + msg = f"unknown color specifier: {repr(color)}" + raise ValueError(msg) def save(self, fp): """Save palette to text file. @@ -168,7 +172,8 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(fp, str): fp = open(fp, "w") fp.write("# Palette\n") @@ -263,6 +268,7 @@ def load(filename): # traceback.print_exc() pass else: - raise OSError("cannot load palette") + msg = "cannot load palette" + raise OSError(msg) return lut # data, rawmode diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index a34678c78..ad607a97b 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -179,7 +179,8 @@ def _toqclass_helper(im): else: if exclusive_fp: im.close() - raise ValueError(f"unsupported image mode {repr(im.mode)}") + msg = f"unsupported image mode {repr(im.mode)}" + raise ValueError(msg) size = im.size __data = data or align8to32(im.tobytes(), size[0], im.mode) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 9df910a43..c4bb6334a 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -30,7 +30,8 @@ class Iterator: def __init__(self, im): if not hasattr(im, "seek"): - raise AttributeError("im must have seek method") + msg = "im must have seek method" + raise AttributeError(msg) self.im = im self.position = getattr(self.im, "_min_frame", 0) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 9d5224588..29d900bef 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -124,7 +124,8 @@ class Viewer: deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) os.system(self.get_command(path, **options)) # nosec return 1 @@ -176,7 +177,8 @@ class MacViewer(Viewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.call(["open", "-a", "Preview.app", path]) executable = sys.executable or shutil.which("python3") if executable: @@ -226,7 +228,8 @@ class XDGViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["xdg-open", path]) return 1 @@ -255,7 +258,8 @@ class DisplayViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) args = ["display"] title = options.get("title") if title: @@ -286,7 +290,8 @@ class GmDisplayViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["gm", "display", path]) return 1 @@ -311,7 +316,8 @@ class EogViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["eog", "-n", path]) return 1 @@ -342,7 +348,8 @@ class XVViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) args = ["xv"] title = options.get("title") if title: diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 1baef7db4..b7ebddf06 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -36,7 +36,8 @@ class Stat: except AttributeError: self.h = image_or_list # assume it to be a histogram list if not isinstance(self.h, list): - raise TypeError("first argument must be image or list") + msg = "first argument must be image or list" + raise TypeError(msg) self.bands = list(range(len(self.h) // 256)) def __getattr__(self, id): diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 949cf1fbf..09a6356fa 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -284,7 +284,8 @@ def _show(image, title): super().__init__(master, image=self.image, bg="black", bd=0) if not tkinter._default_root: - raise OSError("tkinter not initialized") + msg = "tkinter not initialized" + raise OSError(msg) top = tkinter.Toplevel() if title: top.title(title) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index dc7078012..cfeadd53c 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -41,7 +41,8 @@ class ImtImageFile(ImageFile.ImageFile): buffer = self.fp.read(100) if b"\n" not in buffer: - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) xsize = ysize = 0 diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 0bbe50668..774817569 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -66,12 +66,14 @@ class IptcImageFile(ImageFile.ImageFile): # syntax if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9: - raise SyntaxError("invalid IPTC/NAA file") + msg = "invalid IPTC/NAA file" + raise SyntaxError(msg) # field size size = s[3] if size > 132: - raise OSError("illegal field length in IPTC/NAA file") + msg = "illegal field length in IPTC/NAA file" + raise OSError(msg) elif size == 128: size = 0 elif size > 128: @@ -122,7 +124,8 @@ class IptcImageFile(ImageFile.ImageFile): try: compression = COMPRESSION[self.getint((3, 120))] except KeyError as e: - raise OSError("Unknown IPTC image compression") from e + msg = "Unknown IPTC image compression" + raise OSError(msg) from e # tile if tag == (8, 10): diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 11d1d488a..7457874c1 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -44,13 +44,13 @@ class BoxReader: def _read_bytes(self, num_bytes): if not self._can_read(num_bytes): - raise SyntaxError("Not enough data in header") + msg = "Not enough data in header" + raise SyntaxError(msg) data = self.fp.read(num_bytes) if len(data) < num_bytes: - raise OSError( - f"Expected to read {num_bytes} bytes but only got {len(data)}." - ) + msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." + raise OSError(msg) if self.remaining_in_box > 0: self.remaining_in_box -= num_bytes @@ -87,7 +87,8 @@ class BoxReader: hlen = 8 if lbox < hlen or not self._can_read(lbox - hlen): - raise SyntaxError("Invalid header length") + msg = "Invalid header length" + raise SyntaxError(msg) self.remaining_in_box = lbox - hlen return tbox @@ -189,7 +190,8 @@ def _parse_jp2_header(fp): break if size is None or mode is None: - raise SyntaxError("Malformed JP2 header") + msg = "Malformed JP2 header" + raise SyntaxError(msg) return size, mode, mimetype, dpi @@ -217,10 +219,12 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if dpi is not None: self.info["dpi"] = dpi else: - raise SyntaxError("not a JPEG 2000 file") + msg = "not a JPEG 2000 file" + raise SyntaxError(msg) if self.size is None or self.mode is None: - raise SyntaxError("unable to determine size/mode") + msg = "unable to determine size/mode" + raise SyntaxError(msg) self._reduce = 0 self.layers = 0 @@ -312,7 +316,8 @@ def _save(im, fp, filename): ] ) ): - raise ValueError("quality_layers must be a sequence of numbers") + msg = "quality_layers must be a sequence of numbers" + raise ValueError(msg) num_resolutions = info.get("num_resolutions", 0) cblk_size = info.get("codeblock_size", None) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index eb0db5bb3..9657ae9d0 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -204,7 +204,8 @@ def SOF(self, marker): self.bits = s[0] if self.bits != 8: - raise SyntaxError(f"cannot handle {self.bits}-bit layers") + msg = f"cannot handle {self.bits}-bit layers" + raise SyntaxError(msg) self.layers = s[5] if self.layers == 1: @@ -214,7 +215,8 @@ def SOF(self, marker): elif self.layers == 4: self.mode = "CMYK" else: - raise SyntaxError(f"cannot handle {self.layers}-layer images") + msg = f"cannot handle {self.layers}-layer images" + raise SyntaxError(msg) if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: self.info["progressive"] = self.info["progression"] = 1 @@ -253,7 +255,8 @@ def DQT(self, marker): precision = 1 if (v // 16 == 0) else 2 # in bytes qt_length = 1 + precision * 64 if len(s) < qt_length: - raise SyntaxError("bad quantization table marker") + msg = "bad quantization table marker" + raise SyntaxError(msg) data = array.array("B" if precision == 1 else "H", s[1:qt_length]) if sys.byteorder == "little" and precision > 1: data.byteswap() # the values are always big-endian @@ -350,7 +353,8 @@ class JpegImageFile(ImageFile.ImageFile): s = self.fp.read(3) if not _accept(s): - raise SyntaxError("not a JPEG file") + msg = "not a JPEG file" + raise SyntaxError(msg) s = b"\xFF" # Create attributes @@ -394,7 +398,8 @@ class JpegImageFile(ImageFile.ImageFile): elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) s = self.fp.read(1) else: - raise SyntaxError("no marker found") + msg = "no marker found" + raise SyntaxError(msg) def load_read(self, read_bytes): """ @@ -458,7 +463,8 @@ class JpegImageFile(ImageFile.ImageFile): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: - raise ValueError("Invalid Filename") + msg = "Invalid Filename" + raise ValueError(msg) try: with Image.open(path) as _im: @@ -524,12 +530,14 @@ def _getmp(self): info.load(file_contents) mp = dict(info) except Exception as e: - raise SyntaxError("malformed MP Index (unreadable directory)") from e + msg = "malformed MP Index (unreadable directory)" + raise SyntaxError(msg) from e # it's an error not to have a number of images try: quant = mp[0xB001] except KeyError as e: - raise SyntaxError("malformed MP Index (no number of images)") from e + msg = "malformed MP Index (no number of images)" + raise SyntaxError(msg) from e # get MP entries mpentries = [] try: @@ -551,7 +559,8 @@ def _getmp(self): if mpentryattr["ImageDataFormat"] == 0: mpentryattr["ImageDataFormat"] = "JPEG" else: - raise SyntaxError("unsupported picture format in MPO") + msg = "unsupported picture format in MPO" + raise SyntaxError(msg) mptypemap = { 0x000000: "Undefined", 0x010001: "Large Thumbnail (VGA Equivalent)", @@ -566,7 +575,8 @@ def _getmp(self): mpentries.append(mpentry) mp[0xB002] = mpentries except KeyError as e: - raise SyntaxError("malformed MP Index (bad MP Entry)") from e + msg = "malformed MP Index (bad MP Entry)" + raise SyntaxError(msg) from e # Next we should try and parse the individual image unique ID list; # we don't because I've never seen this actually used in a real MPO # file and so can't test it. @@ -626,12 +636,14 @@ def get_sampling(im): def _save(im, fp, filename): if im.width == 0 or im.height == 0: - raise ValueError("cannot write empty image as JPEG") + msg = "cannot write empty image as JPEG" + raise ValueError(msg) try: rawmode = RAWMODE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as JPEG") from e + msg = f"cannot write mode {im.mode} as JPEG" + raise OSError(msg) from e info = im.encoderinfo @@ -651,7 +663,8 @@ def _save(im, fp, filename): subsampling = preset.get("subsampling", -1) qtables = preset.get("quantization") elif not isinstance(quality, int): - raise ValueError("Invalid quality setting") + msg = "Invalid quality setting" + raise ValueError(msg) else: if subsampling in presets: subsampling = presets[subsampling].get("subsampling", -1) @@ -670,7 +683,8 @@ def _save(im, fp, filename): subsampling = 2 elif subsampling == "keep": if im.format != "JPEG": - raise ValueError("Cannot use 'keep' when original image is not a JPEG") + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) subsampling = get_sampling(im) def validate_qtables(qtables): @@ -684,7 +698,8 @@ def _save(im, fp, filename): for num in line.split("#", 1)[0].split() ] except ValueError as e: - raise ValueError("Invalid quantization table") from e + msg = "Invalid quantization table" + raise ValueError(msg) from e else: qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): @@ -695,21 +710,24 @@ def _save(im, fp, filename): elif isinstance(qtables, tuple): qtables = list(qtables) if not (0 < len(qtables) < 5): - raise ValueError("None or too many quantization tables") + msg = "None or too many quantization tables" + raise ValueError(msg) for idx, table in enumerate(qtables): try: if len(table) != 64: raise TypeError table = array.array("H", table) except TypeError as e: - raise ValueError("Invalid quantization table") from e + msg = "Invalid quantization table" + raise ValueError(msg) from e else: qtables[idx] = list(table) return qtables if qtables == "keep": if im.format != "JPEG": - raise ValueError("Cannot use 'keep' when original image is not a JPEG") + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index cd047fe9d..8d4d826aa 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -39,7 +39,8 @@ class McIdasImageFile(ImageFile.ImageFile): # parse area file directory s = self.fp.read(256) if not _accept(s) or len(s) != 256: - raise SyntaxError("not an McIdas area file") + msg = "not an McIdas area file" + raise SyntaxError(msg) self.area_descriptor_raw = s self.area_descriptor = w = [0] + list(struct.unpack("!64i", s)) @@ -56,7 +57,8 @@ class McIdasImageFile(ImageFile.ImageFile): mode = "I" rawmode = "I;32B" else: - raise SyntaxError("unsupported McIdas format") + msg = "unsupported McIdas format" + raise SyntaxError(msg) self.mode = mode self._size = w[10], w[9] diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index d4f6c90f7..e7e1054a3 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -47,7 +47,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: - raise SyntaxError("not an MIC file; invalid OLE file") from e + msg = "not an MIC file; invalid OLE file" + raise SyntaxError(msg) from e # find ACI subfiles with Image members (maybe not the # best way to identify MIC files, but what the... ;-) @@ -60,7 +61,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): # if we didn't find any images, this is probably not # an MIC file. if not self.images: - raise SyntaxError("not an MIC file; no image entries") + msg = "not an MIC file; no image entries" + raise SyntaxError(msg) self.frame = None self._n_frames = len(self.images) @@ -77,7 +79,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: filename = self.images[frame] except IndexError as e: - raise EOFError("no such frame") from e + msg = "no such frame" + raise EOFError(msg) from e self.fp = self.ole.openstream(filename) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index a358dfdce..2d799d6d8 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -67,7 +67,8 @@ class MpegImageFile(ImageFile.ImageFile): s = BitStream(self.fp) if s.read(32) != 0x1B3: - raise SyntaxError("not an MPEG file") + msg = "not an MPEG file" + raise SyntaxError(msg) self.mode = "RGB" self._size = s.read(12), s.read(12) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 095cfe7ee..b1ec2c7bc 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -143,7 +143,8 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.fp.seek(self.offset + 2) # skip SOI marker segment = self.fp.read(2) if not segment: - raise ValueError("No data found for frame") + msg = "No data found for frame" + raise ValueError(msg) self._size = self._initial_size if i16(segment) == 0xFFE1: # APP1 n = i16(self.fp.read(2)) - 2 diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index c4d7ddbb4..5420894dc 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -53,14 +53,16 @@ class MspImageFile(ImageFile.ImageFile): # Header s = self.fp.read(32) if not _accept(s): - raise SyntaxError("not an MSP file") + msg = "not an MSP file" + raise SyntaxError(msg) # Header checksum checksum = 0 for i in range(0, 32, 2): checksum = checksum ^ i16(s, i) if checksum != 0: - raise SyntaxError("bad MSP checksum") + msg = "bad MSP checksum" + raise SyntaxError(msg) self.mode = "1" self._size = i16(s, 4), i16(s, 6) @@ -118,7 +120,8 @@ class MspDecoder(ImageFile.PyDecoder): f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2) ) except struct.error as e: - raise OSError("Truncated MSP file in row map") from e + msg = "Truncated MSP file in row map" + raise OSError(msg) from e for x, rowlen in enumerate(rowmap): try: @@ -127,9 +130,8 @@ class MspDecoder(ImageFile.PyDecoder): continue row = self.fd.read(rowlen) if len(row) != rowlen: - raise OSError( - "Truncated MSP file, expected %d bytes on row %s", (rowlen, x) - ) + msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}" + raise OSError(msg) idx = 0 while idx < rowlen: runtype = row[idx] @@ -144,7 +146,8 @@ class MspDecoder(ImageFile.PyDecoder): idx += runcount except struct.error as e: - raise OSError(f"Corrupted MSP file in row {x}") from e + msg = f"Corrupted MSP file in row {x}" + raise OSError(msg) from e self.set_as_raw(img.getvalue(), ("1", 0, 1)) @@ -161,7 +164,8 @@ Image.register_decoder("MSP", MspDecoder) def _save(im, fp, filename): if im.mode != "1": - raise OSError(f"cannot write mode {im.mode} as MSP") + msg = f"cannot write mode {im.mode} as MSP" + raise OSError(msg) # create MSP header header = [0] * 16 diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index ee9dca860..07acd5580 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -34,7 +34,8 @@ class PaletteFile: if s[:1] == b"#": continue if len(s) > 100: - raise SyntaxError("bad palette file") + msg = "bad palette file" + raise SyntaxError(msg) v = [int(x) for x in s.split()] try: diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 700f10e3f..109aad9ab 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -138,7 +138,8 @@ def _save(im, fp, filename): bpp = im.info["bpp"] im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) else: - raise OSError(f"cannot write mode {im.mode} as Palm") + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) # we ignore the palette here im.mode = "P" @@ -154,7 +155,8 @@ def _save(im, fp, filename): else: - raise OSError(f"cannot write mode {im.mode} as Palm") + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) # # make sure image data is available diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 38caf5c63..5802d386a 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -35,7 +35,8 @@ class PcdImageFile(ImageFile.ImageFile): s = self.fp.read(2048) if s[:4] != b"PCD_": - raise SyntaxError("not a PCD file") + msg = "not a PCD file" + raise SyntaxError(msg) orientation = s[1538] & 3 self.tile_post_rotate = None diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 442ac70c4..ecce1b097 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -63,7 +63,8 @@ class PcfFontFile(FontFile.FontFile): magic = l32(fp.read(4)) if magic != PCF_MAGIC: - raise SyntaxError("not a PCF file") + msg = "not a PCF file" + raise SyntaxError(msg) super().__init__() @@ -186,7 +187,8 @@ class PcfFontFile(FontFile.FontFile): nbitmaps = i32(fp.read(4)) if nbitmaps != len(metrics): - raise OSError("Wrong number of bitmaps") + msg = "Wrong number of bitmaps" + raise OSError(msg) offsets = [] for i in range(nbitmaps): diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 841c18a22..3202475dc 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,12 +54,14 @@ class PcxImageFile(ImageFile.ImageFile): # header s = self.fp.read(128) if not _accept(s): - raise SyntaxError("not a PCX file") + msg = "not a PCX file" + raise SyntaxError(msg) # image bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: - raise SyntaxError("bad PCX image size") + msg = "bad PCX image size" + raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) # format @@ -105,7 +107,8 @@ class PcxImageFile(ImageFile.ImageFile): rawmode = "RGB;L" else: - raise OSError("unknown PCX mode") + msg = "unknown PCX mode" + raise OSError(msg) self.mode = mode self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] @@ -144,7 +147,8 @@ def _save(im, fp, filename): try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: - raise ValueError(f"Cannot save {im.mode} images as PCX") from e + msg = f"Cannot save {im.mode} images as PCX" + raise ValueError(msg) from e # bytes per plane stride = (im.size[0] * bits + 7) // 8 diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 404759a7f..baad4939f 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -174,7 +174,8 @@ def _save(im, fp, filename, save_all=False): procset = "ImageC" # color images decode = [1, 0, 1, 0, 1, 0, 1, 0] else: - raise ValueError(f"cannot save mode {im.mode}") + msg = f"cannot save mode {im.mode}" + raise ValueError(msg) # # image @@ -198,7 +199,8 @@ def _save(im, fp, filename, save_all=False): elif filter == "RunLengthDecode": ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) else: - raise ValueError(f"unsupported PDF filter ({filter})") + msg = f"unsupported PDF filter ({filter})" + raise ValueError(msg) stream = op.getvalue() if filter == "CCITTFaxDecode": diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index fd5cc5a61..e4a0f25a9 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -138,9 +138,10 @@ class XrefTable: elif key in self.deleted_entries: generation = self.deleted_entries[key] else: - raise IndexError( + msg = ( "object ID " + str(key) + " cannot be deleted because it doesn't exist" ) + raise IndexError(msg) def __contains__(self, key): return key in self.existing_entries or key in self.new_entries @@ -314,9 +315,8 @@ class PdfStream: expected_length = self.dictionary.Length return zlib.decompress(self.buf, bufsize=int(expected_length)) else: - raise NotImplementedError( - f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" - ) + msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" + raise NotImplementedError(msg) def pdf_repr(x): @@ -358,7 +358,8 @@ class PdfParser: def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): if buf and f: - raise RuntimeError("specify buf or f or filename, but not both buf and f") + msg = "specify buf or f or filename, but not both buf and f" + raise RuntimeError(msg) self.filename = filename self.buf = buf self.f = f @@ -920,7 +921,8 @@ class PdfParser: result.extend(b")") nesting_depth -= 1 offset = m.end() - raise PdfFormatError("unfinished literal string") + msg = "unfinished literal string" + raise PdfFormatError(msg) re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) re_xref_subsection_start = re.compile( diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index c4860b6c4..8d0a34dba 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -44,7 +44,8 @@ class PixarImageFile(ImageFile.ImageFile): # assuming a 4-byte magic label s = self.fp.read(4) if not _accept(s): - raise SyntaxError("not a PIXAR file") + msg = "not a PIXAR file" + raise SyntaxError(msg) # read rest of header s = s + self.fp.read(508) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index b6a3c4cb6..b6626bbc5 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -138,14 +138,16 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) def _safe_zlib_decompress(s): dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: - raise ValueError("Decompressed Data Too Large") + msg = "Decompressed Data Too Large" + raise ValueError(msg) return plaintext @@ -178,7 +180,8 @@ class ChunkStream: if not is_cid(cid): if not ImageFile.LOAD_TRUNCATED_IMAGES: - raise SyntaxError(f"broken PNG file (chunk {repr(cid)})") + msg = f"broken PNG file (chunk {repr(cid)})" + raise SyntaxError(msg) return cid, pos, length @@ -215,13 +218,11 @@ class ChunkStream: crc1 = _crc32(data, _crc32(cid)) crc2 = i32(self.fp.read(4)) if crc1 != crc2: - raise SyntaxError( - f"broken PNG file (bad header checksum in {repr(cid)})" - ) + msg = f"broken PNG file (bad header checksum in {repr(cid)})" + raise SyntaxError(msg) except struct.error as e: - raise SyntaxError( - f"broken PNG file (incomplete checksum in {repr(cid)})" - ) from e + msg = f"broken PNG file (incomplete checksum in {repr(cid)})" + raise SyntaxError(msg) from e def crc_skip(self, cid, data): """Read checksum""" @@ -239,7 +240,8 @@ class ChunkStream: try: cid, pos, length = self.read() except struct.error as e: - raise OSError("truncated PNG file") from e + msg = "truncated PNG file" + raise OSError(msg) from e if cid == endchunk: break @@ -376,10 +378,11 @@ class PngStream(ChunkStream): def check_text_memory(self, chunklen): self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: - raise ValueError( + msg = ( "Too much memory used in text chunks: " f"{self.text_memory}>MAX_TEXT_MEMORY" ) + raise ValueError(msg) def save_rewind(self): self.rewind_state = { @@ -407,7 +410,8 @@ class PngStream(ChunkStream): logger.debug("Compression method %s", s[i]) comp_method = s[i] if comp_method != 0: - raise SyntaxError(f"Unknown compression method {comp_method} in iCCP chunk") + msg = f"Unknown compression method {comp_method} in iCCP chunk" + raise SyntaxError(msg) try: icc_profile = _safe_zlib_decompress(s[i + 2 :]) except ValueError: @@ -427,7 +431,8 @@ class PngStream(ChunkStream): if length < 13: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated IHDR chunk") + msg = "Truncated IHDR chunk" + raise ValueError(msg) self.im_size = i32(s, 0), i32(s, 4) try: self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] @@ -436,7 +441,8 @@ class PngStream(ChunkStream): if s[12]: self.im_info["interlace"] = 1 if s[11]: - raise SyntaxError("unknown filter category") + msg = "unknown filter category" + raise SyntaxError(msg) return s def chunk_IDAT(self, pos, length): @@ -512,7 +518,8 @@ class PngStream(ChunkStream): if length < 1: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated sRGB chunk") + msg = "Truncated sRGB chunk" + raise ValueError(msg) self.im_info["srgb"] = s[0] return s @@ -523,7 +530,8 @@ class PngStream(ChunkStream): if length < 9: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated pHYs chunk") + msg = "Truncated pHYs chunk" + raise ValueError(msg) px, py = i32(s, 0), i32(s, 4) unit = s[8] if unit == 1: # meter @@ -567,7 +575,8 @@ class PngStream(ChunkStream): else: comp_method = 0 if comp_method != 0: - raise SyntaxError(f"Unknown compression method {comp_method} in zTXt chunk") + msg = f"Unknown compression method {comp_method} in zTXt chunk" + raise SyntaxError(msg) try: v = _safe_zlib_decompress(v[1:]) except ValueError: @@ -639,7 +648,8 @@ class PngStream(ChunkStream): if length < 8: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("APNG contains truncated acTL chunk") + msg = "APNG contains truncated acTL chunk" + raise ValueError(msg) if self.im_n_frames is not None: self.im_n_frames = None warnings.warn("Invalid APNG, will use default PNG image if possible") @@ -658,18 +668,21 @@ class PngStream(ChunkStream): if length < 26: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("APNG contains truncated fcTL chunk") + msg = "APNG contains truncated fcTL chunk" + raise ValueError(msg) seq = i32(s) if (self._seq_num is None and seq != 0) or ( self._seq_num is not None and self._seq_num != seq - 1 ): - raise SyntaxError("APNG contains frame sequence errors") + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) self._seq_num = seq width, height = i32(s, 4), i32(s, 8) px, py = i32(s, 12), i32(s, 16) im_w, im_h = self.im_size if px + width > im_w or py + height > im_h: - raise SyntaxError("APNG contains invalid frames") + msg = "APNG contains invalid frames" + raise SyntaxError(msg) self.im_info["bbox"] = (px, py, px + width, py + height) delay_num, delay_den = i16(s, 20), i16(s, 22) if delay_den == 0: @@ -684,11 +697,13 @@ class PngStream(ChunkStream): if ImageFile.LOAD_TRUNCATED_IMAGES: s = ImageFile._safe_read(self.fp, length) return s - raise ValueError("APNG contains truncated fDAT chunk") + msg = "APNG contains truncated fDAT chunk" + raise ValueError(msg) s = ImageFile._safe_read(self.fp, 4) seq = i32(s) if self._seq_num != seq - 1: - raise SyntaxError("APNG contains frame sequence errors") + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) self._seq_num = seq return self.chunk_IDAT(pos + 4, length - 4) @@ -713,7 +728,8 @@ class PngImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(8)): - raise SyntaxError("not a PNG file") + msg = "not a PNG file" + raise SyntaxError(msg) self._fp = self.fp self.__frame = 0 @@ -797,7 +813,8 @@ class PngImageFile(ImageFile.ImageFile): """Verify PNG file""" if self.fp is None: - raise RuntimeError("verify must be called directly after open") + msg = "verify must be called directly after open" + raise RuntimeError(msg) # back up to beginning of IDAT block self.fp.seek(self.tile[0][2] - 8) @@ -821,7 +838,8 @@ class PngImageFile(ImageFile.ImageFile): self._seek(f) except EOFError as e: self.seek(last_frame) - raise EOFError("no more images in APNG file") from e + msg = "no more images in APNG file" + raise EOFError(msg) from e def _seek(self, frame, rewind=False): if frame == 0: @@ -844,7 +862,8 @@ class PngImageFile(ImageFile.ImageFile): self.__frame = 0 else: if frame != self.__frame + 1: - raise ValueError(f"cannot seek to frame {frame}") + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) # ensure previous frame was loaded self.load() @@ -869,11 +888,13 @@ class PngImageFile(ImageFile.ImageFile): break if cid == b"IEND": - raise EOFError("No more images in APNG file") + msg = "No more images in APNG file" + raise EOFError(msg) if cid == b"fcTL": if frame_start: # there must be at least one fdAT chunk between fcTL chunks - raise SyntaxError("APNG missing frame data") + msg = "APNG missing frame data" + raise SyntaxError(msg) frame_start = True try: @@ -1277,7 +1298,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): try: rawmode, mode = _OUTMODES[mode] except KeyError as e: - raise OSError(f"cannot write mode {mode} as PNG") from e + msg = f"cannot write mode {mode} as PNG" + raise OSError(msg) from e # # write minimal PNG file @@ -1358,7 +1380,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): if "transparency" in im.encoderinfo: # don't bother with transparency if it's an RGBA # and it's in the info dict. It's probably just stale. - raise OSError("cannot use transparency for this mode") + msg = "cannot use transparency for this mode" + raise OSError(msg) else: if im.mode == "P" and im.im.getpalettemode() == "RGBA": alpha = im.im.getpalette("RGBA", "A") diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 1670d9d64..dee2f1e15 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -84,9 +84,11 @@ class PpmImageFile(ImageFile.ImageFile): token += c if not token: # Token was not even 1 byte - raise ValueError("Reached EOF while reading header") + msg = "Reached EOF while reading header" + raise ValueError(msg) elif len(token) > 10: - raise ValueError(f"Token too long in file header: {token.decode()}") + msg = f"Token too long in file header: {token.decode()}" + raise ValueError(msg) return token def _open(self): @@ -94,7 +96,8 @@ class PpmImageFile(ImageFile.ImageFile): try: mode = MODES[magic_number] except KeyError: - raise SyntaxError("not a PPM file") + msg = "not a PPM file" + raise SyntaxError(msg) if magic_number in (b"P1", b"P4"): self.custom_mimetype = "image/x-portable-bitmap" @@ -122,9 +125,8 @@ class PpmImageFile(ImageFile.ImageFile): elif ix == 2: # token is maxval maxval = token if not 0 < maxval < 65536: - raise ValueError( - "maxval must be greater than 0 and less than 65536" - ) + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) if maxval > 255 and mode == "L": self.mode = "I" @@ -208,9 +210,8 @@ class PpmPlainDecoder(ImageFile.PyDecoder): tokens = b"".join(block.split()) for token in tokens: if token not in (48, 49): - raise ValueError( - b"Invalid token for this mode: %s" % bytes([token]) - ) + msg = b"Invalid token for this mode: %s" % bytes([token]) + raise ValueError(msg) data = (data + tokens)[:total_bytes] invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) @@ -243,18 +244,19 @@ class PpmPlainDecoder(ImageFile.PyDecoder): if block and not block[-1:].isspace(): # block might split token half_token = tokens.pop() # save half token for later if len(half_token) > max_len: # prevent buildup of half_token - raise ValueError( + msg = ( b"Token too long found in data: %s" % half_token[: max_len + 1] ) + raise ValueError(msg) for token in tokens: if len(token) > max_len: - raise ValueError( - b"Token too long found in data: %s" % token[: max_len + 1] - ) + msg = b"Token too long found in data: %s" % token[: max_len + 1] + raise ValueError(msg) value = int(token) if value > maxval: - raise ValueError(f"Channel value too large for this mode: {value}") + msg = f"Channel value too large for this mode: {value}" + raise ValueError(msg) value = round(value / maxval * out_max) data += o32(value) if self.mode == "I" else o8(value) if len(data) == total_bytes: # finished! @@ -314,7 +316,8 @@ def _save(im, fp, filename): elif im.mode in ("RGB", "RGBA"): rawmode, head = "RGB", b"P6" else: - raise OSError(f"cannot write mode {im.mode} as PPM") + msg = f"cannot write mode {im.mode} as PPM" + raise OSError(msg) fp.write(head + b"\n%d %d\n" % im.size) if head == b"P6": fp.write(b"255\n") diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index bd10e3b95..c1ca30a03 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -65,7 +65,8 @@ class PsdImageFile(ImageFile.ImageFile): s = read(26) if not _accept(s) or i16(s, 4) != 1: - raise SyntaxError("not a PSD file") + msg = "not a PSD file" + raise SyntaxError(msg) psd_bits = i16(s, 22) psd_channels = i16(s, 12) @@ -74,7 +75,8 @@ class PsdImageFile(ImageFile.ImageFile): mode, channels = MODES[(psd_mode, psd_bits)] if channels > psd_channels: - raise OSError("not enough channels") + msg = "not enough channels" + raise OSError(msg) if mode == "RGB" and psd_channels == 4: mode = "RGBA" channels = 4 @@ -152,7 +154,8 @@ class PsdImageFile(ImageFile.ImageFile): self.fp = self._fp return name, bbox except IndexError as e: - raise EOFError("no such layer") from e + msg = "no such layer" + raise EOFError(msg) from e def tell(self): # return layer number (0=image, 1..max=layers) @@ -170,7 +173,8 @@ def _layerinfo(fp, ct_bytes): # sanity check if ct_bytes < (abs(ct) * 20): - raise SyntaxError("Layer block too short for number of layers requested") + msg = "Layer block too short for number of layers requested" + raise SyntaxError(msg) for _ in range(abs(ct)): diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 039f5ceea..e9cb34ced 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -79,7 +79,8 @@ class PyAccess: :param color: The pixel value. """ if self.readonly: - raise ValueError("Attempt to putpixel a read only image") + msg = "Attempt to putpixel a read only image" + raise ValueError(msg) (x, y) = xy if x < 0: x = self.xsize + x @@ -127,7 +128,8 @@ class PyAccess: def check_xy(self, xy): (x, y) = xy if not (0 <= x < self.xsize and 0 <= y < self.ysize): - raise ValueError("pixel location out of range") + msg = "pixel location out of range" + raise ValueError(msg) return xy diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index f0207bb77..d533c55e5 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -60,7 +60,8 @@ class SgiImageFile(ImageFile.ImageFile): s = self.fp.read(headlen) if not _accept(s): - raise ValueError("Not an SGI image file") + msg = "Not an SGI image file" + raise ValueError(msg) # compression : verbatim or RLE compression = s[2] @@ -91,7 +92,8 @@ class SgiImageFile(ImageFile.ImageFile): pass if rawmode == "": - raise ValueError("Unsupported SGI image mode") + msg = "Unsupported SGI image mode" + raise ValueError(msg) self._size = xsize, ysize self.mode = rawmode.split(";")[0] @@ -124,7 +126,8 @@ class SgiImageFile(ImageFile.ImageFile): def _save(im, fp, filename): if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L": - raise ValueError("Unsupported SGI image mode") + msg = "Unsupported SGI image mode" + raise ValueError(msg) # Get the keyword arguments info = im.encoderinfo @@ -133,7 +136,8 @@ def _save(im, fp, filename): bpc = info.get("bpc", 1) if bpc not in (1, 2): - raise ValueError("Unsupported number of bytes per pixel") + msg = "Unsupported number of bytes per pixel" + raise ValueError(msg) # Flip the image, since the origin of SGI file is the bottom-left corner orientation = -1 @@ -158,9 +162,8 @@ def _save(im, fp, filename): # assert we've got the right number of bands. if len(im.getbands()) != z: - raise ValueError( - f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" - ) + msg = f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" + raise ValueError(msg) # Minimum Byte value pinmin = 0 diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index acafc320e..1192c2d73 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -110,14 +110,17 @@ class SpiderImageFile(ImageFile.ImageFile): t = struct.unpack("<27f", f) # little-endian hdrlen = isSpiderHeader(t) if hdrlen == 0: - raise SyntaxError("not a valid Spider file") + msg = "not a valid Spider file" + raise SyntaxError(msg) except struct.error as e: - raise SyntaxError("not a valid Spider file") from e + msg = "not a valid Spider file" + raise SyntaxError(msg) from e h = (99,) + t # add 1 value : spider header index starts at 1 iform = int(h[5]) if iform != 1: - raise SyntaxError("not a Spider 2D image") + msg = "not a Spider 2D image" + raise SyntaxError(msg) self._size = int(h[12]), int(h[2]) # size in pixels (width, height) self.istack = int(h[24]) @@ -140,7 +143,8 @@ class SpiderImageFile(ImageFile.ImageFile): offset = hdrlen + self.stkoffset self.istack = 2 # So Image knows it's still a stack else: - raise SyntaxError("inconsistent stack header values") + msg = "inconsistent stack header values" + raise SyntaxError(msg) if self.bigendian: self.rawmode = "F;32BF" @@ -168,7 +172,8 @@ class SpiderImageFile(ImageFile.ImageFile): def seek(self, frame): if self.istack == 0: - raise EOFError("attempt to seek in a non-stack file") + msg = "attempt to seek in a non-stack file" + raise EOFError(msg) if not self._seek_check(frame): return self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) @@ -260,7 +265,8 @@ def _save(im, fp, filename): hdr = makeSpiderHeader(im) if len(hdr) < 256: - raise OSError("Error creating Spider header") + msg = "Error creating Spider header" + raise OSError(msg) # write the SPIDER header fp.writelines(hdr) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index c03759a01..c64de4444 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -54,7 +54,8 @@ class SunImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(32) if not _accept(s): - raise SyntaxError("not an SUN raster file") + msg = "not an SUN raster file" + raise SyntaxError(msg) offset = 32 @@ -83,14 +84,17 @@ class SunImageFile(ImageFile.ImageFile): else: self.mode, rawmode = "RGB", "BGRX" else: - raise SyntaxError("Unsupported Mode/Bit Depth") + msg = "Unsupported Mode/Bit Depth" + raise SyntaxError(msg) if palette_length: if palette_length > 1024: - raise SyntaxError("Unsupported Color Palette Length") + msg = "Unsupported Color Palette Length" + raise SyntaxError(msg) if palette_type != 1: - raise SyntaxError("Unsupported Palette Type") + msg = "Unsupported Palette Type" + raise SyntaxError(msg) offset = offset + palette_length self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) @@ -124,7 +128,8 @@ class SunImageFile(ImageFile.ImageFile): elif file_type == 2: self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] else: - raise SyntaxError("Unsupported Sun Raster file type") + msg = "Unsupported Sun Raster file type" + raise SyntaxError(msg) # diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index d108362fc..20e8a083f 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,14 @@ class TarIO(ContainerIO.ContainerIO): s = self.fh.read(512) if len(s) != 512: - raise OSError("unexpected end of tar file") + msg = "unexpected end of tar file" + raise OSError(msg) name = s[:100].decode("utf-8") i = name.find("\0") if i == 0: - raise OSError("cannot find subfile") + msg = "cannot find subfile" + raise OSError(msg) if i > 0: name = name[:i] diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index cd454b755..53fe6ef5c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -73,7 +73,8 @@ class TgaImageFile(ImageFile.ImageFile): or self.size[1] <= 0 or depth not in (1, 8, 16, 24, 32) ): - raise SyntaxError("not a TGA file") + msg = "not a TGA file" + raise SyntaxError(msg) # image mode if imagetype in (3, 11): @@ -89,7 +90,8 @@ class TgaImageFile(ImageFile.ImageFile): if depth == 32: self.mode = "RGBA" else: - raise SyntaxError("unknown TGA mode") + msg = "unknown TGA mode" + raise SyntaxError(msg) # orientation orientation = flags & 0x30 @@ -99,7 +101,8 @@ class TgaImageFile(ImageFile.ImageFile): elif orientation in [0, 0x10]: orientation = -1 else: - raise SyntaxError("unknown TGA orientation") + msg = "unknown TGA orientation" + raise SyntaxError(msg) self.info["orientation"] = orientation @@ -175,7 +178,8 @@ def _save(im, fp, filename): try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as TGA") from e + msg = f"cannot write mode {im.mode} as TGA" + raise OSError(msg) from e if "rle" in im.encoderinfo: rle = im.encoderinfo["rle"] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fa3479b35..431edfd9b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -500,14 +500,16 @@ class ImageFileDirectory_v2(MutableMapping): :param prefix: Override the endianness of the file. """ if not _accept(ifh): - raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)") + msg = f"not a TIFF file (header {repr(ifh)} not valid)" + raise SyntaxError(msg) self._prefix = prefix if prefix is not None else ifh[:2] if self._prefix == MM: self._endian = ">" elif self._prefix == II: self._endian = "<" else: - raise SyntaxError("not a TIFF IFD") + msg = "not a TIFF IFD" + raise SyntaxError(msg) self._bigtiff = ifh[2] == 43 self.group = group self.tagtype = {} @@ -524,7 +526,8 @@ class ImageFileDirectory_v2(MutableMapping): @legacy_api.setter def legacy_api(self, value): - raise Exception("Not allowing setting of legacy api") + msg = "Not allowing setting of legacy api" + raise Exception(msg) def reset(self): self._tags_v1 = {} # will remain empty if legacy_api is false @@ -780,10 +783,11 @@ class ImageFileDirectory_v2(MutableMapping): def _ensure_read(self, fp, size): ret = fp.read(size) if len(ret) != size: - raise OSError( + msg = ( "Corrupt EXIF data. " f"Expecting to read {size} bytes but only got {len(ret)}. " ) + raise OSError(msg) return ret def load(self, fp): @@ -910,7 +914,8 @@ class ImageFileDirectory_v2(MutableMapping): if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: - raise NotImplementedError("multistrip support not yet implemented") + msg = "multistrip support not yet implemented" + raise NotImplementedError(msg) value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data @@ -1123,7 +1128,8 @@ class TiffImageFile(ImageFile.ImageFile): while len(self._frame_pos) <= frame: if not self.__next: - raise EOFError("no more images in TIFF file") + msg = "no more images in TIFF file" + raise EOFError(msg) logger.debug( f"Seeking to frame {frame}, on frame {self.__frame}, " f"__next {self.__next}, location: {self.fp.tell()}" @@ -1230,7 +1236,8 @@ class TiffImageFile(ImageFile.ImageFile): self.load_prepare() if not len(self.tile) == 1: - raise OSError("Not exactly one tile") + msg = "Not exactly one tile" + raise OSError(msg) # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) @@ -1262,7 +1269,8 @@ class TiffImageFile(ImageFile.ImageFile): try: decoder.setimage(self.im, extents) except ValueError as e: - raise OSError("Couldn't set the image") from e + msg = "Couldn't set the image" + raise OSError(msg) from e close_self_fp = self._exclusive_fp and not self.is_animated if hasattr(self.fp, "getvalue"): @@ -1316,7 +1324,8 @@ class TiffImageFile(ImageFile.ImageFile): """Setup this image object based on current tags""" if 0xBC01 in self.tag_v2: - raise OSError("Windows Media Photo files not yet supported") + msg = "Windows Media Photo files not yet supported" + raise OSError(msg) # extract relevant tags self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] @@ -1375,7 +1384,8 @@ class TiffImageFile(ImageFile.ImageFile): logger.error( "More samples per pixel than can be decoded: %s", samples_per_pixel ) - raise SyntaxError("Invalid value for samples per pixel") + msg = "Invalid value for samples per pixel" + raise SyntaxError(msg) if samples_per_pixel < bps_actual_count: # If a file has more values in bps_tuple than expected, @@ -1387,7 +1397,8 @@ class TiffImageFile(ImageFile.ImageFile): bps_tuple = bps_tuple * samples_per_pixel if len(bps_tuple) != samples_per_pixel: - raise SyntaxError("unknown data organization") + msg = "unknown data organization" + raise SyntaxError(msg) # mode: check photometric interpretation and bits per pixel key = ( @@ -1403,7 +1414,8 @@ class TiffImageFile(ImageFile.ImageFile): self.mode, rawmode = OPEN_INFO[key] except KeyError as e: logger.debug("- unsupported format") - raise SyntaxError("unknown pixel mode") from e + msg = "unknown pixel mode" + raise SyntaxError(msg) from e logger.debug(f"- raw mode: {rawmode}") logger.debug(f"- pil mode: {self.mode}") @@ -1519,7 +1531,8 @@ class TiffImageFile(ImageFile.ImageFile): layer += 1 else: logger.debug("- unsupported data organization") - raise SyntaxError("unknown data organization") + msg = "unknown data organization" + raise SyntaxError(msg) # Fix up info. if ICCPROFILE in self.tag_v2: @@ -1571,7 +1584,8 @@ def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as TIFF") from e + msg = f"cannot write mode {im.mode} as TIFF" + raise OSError(msg) from e ifd = ImageFileDirectory_v2(prefix=prefix) @@ -1736,11 +1750,11 @@ def _save(im, fp, filename): if "quality" in encoderinfo: quality = encoderinfo["quality"] if not isinstance(quality, int) or quality < 0 or quality > 100: - raise ValueError("Invalid quality setting") + msg = "Invalid quality setting" + raise ValueError(msg) if compression != "jpeg": - raise ValueError( - "quality setting only supported for 'jpeg' compression" - ) + msg = "quality setting only supported for 'jpeg' compression" + raise ValueError(msg) ifd[JPEGQUALITY] = quality logger.debug("Saving using libtiff encoder") @@ -1837,7 +1851,8 @@ def _save(im, fp, filename): if s: break if s < 0: - raise OSError(f"encoder error {s} when writing image file") + msg = f"encoder error {s} when writing image file" + raise OSError(msg) else: for tag in blocklist: @@ -1912,7 +1927,8 @@ class AppendingTiffWriter: elif iimm == b"MM\x00\x2a": self.setEndian(">") else: - raise RuntimeError("Invalid TIFF file header") + msg = "Invalid TIFF file header" + raise RuntimeError(msg) self.skipIFDs() self.goToEnd() @@ -1926,12 +1942,14 @@ class AppendingTiffWriter: iimm = self.f.read(4) if not iimm: - # raise RuntimeError("nothing written into new page") + # msg = "nothing written into new page" + # raise RuntimeError(msg) # Make it easy to finish a frame without committing to a new one. return if iimm != self.IIMM: - raise RuntimeError("IIMM of new page doesn't match IIMM of first page") + msg = "IIMM of new page doesn't match IIMM of first page" + raise RuntimeError(msg) ifd_offset = self.readLong() ifd_offset += self.offsetOfNewPage @@ -2005,29 +2023,34 @@ class AppendingTiffWriter: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def rewriteLastShort(self, value): self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.shortFmt, value)) if bytes_written is not None and bytes_written != 2: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2") + msg = f"wrote only {bytes_written} bytes but wanted 2" + raise RuntimeError(msg) def rewriteLastLong(self, value): self.f.seek(-4, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def writeShort(self, value): bytes_written = self.f.write(struct.pack(self.shortFmt, value)) if bytes_written is not None and bytes_written != 2: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2") + msg = f"wrote only {bytes_written} bytes but wanted 2" + raise RuntimeError(msg) def writeLong(self, value): bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def close(self): self.finalize() @@ -2070,7 +2093,8 @@ class AppendingTiffWriter: def fixOffsets(self, count, isShort=False, isLong=False): if not isShort and not isLong: - raise RuntimeError("offset is neither short nor long") + msg = "offset is neither short nor long" + raise RuntimeError(msg) for i in range(count): offset = self.readShort() if isShort else self.readLong() @@ -2078,7 +2102,8 @@ class AppendingTiffWriter: if isShort and offset >= 65536: # offset is now too large - we must convert shorts to longs if count != 1: - raise RuntimeError("not implemented") # XXX TODO + msg = "not implemented" + raise RuntimeError(msg) # XXX TODO # simple case - the offset is just one and therefore it is # local (not referenced with another offset) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 81ed550d9..1d074f78c 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -130,7 +130,8 @@ class WebPImageFile(ImageFile.ImageFile): if ret is None: self._reset() # Reset just to be safe self.seek(0) - raise EOFError("failed to decode next frame in WebP file") + msg = "failed to decode next frame in WebP file" + raise EOFError(msg) # Compute duration data, timestamp = ret @@ -233,9 +234,8 @@ def _save_all(im, fp, filename): or len(background) != 4 or not all(0 <= v < 256 for v in background) ): - raise OSError( - f"Background color is not an RGBA tuple clamped to (0-255): {background}" - ) + msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}" + raise OSError(msg) # Convert to packed uint bg_r, bg_g, bg_b, bg_a = background @@ -311,7 +311,8 @@ def _save_all(im, fp, filename): # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) if data is None: - raise OSError("cannot write file as WebP (encoder returned None)") + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) fp.write(data) @@ -351,7 +352,8 @@ def _save(im, fp, filename): xmp, ) if data is None: - raise OSError("cannot write file as WebP (encoder returned None)") + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) fp.write(data) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 2f54cdebb..639730b8e 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -109,7 +109,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": - raise SyntaxError("Unsupported WMF file format") + msg = "Unsupported WMF file format" + raise SyntaxError(msg) elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": # enhanced metafile @@ -137,7 +138,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = xdpi, ydpi else: - raise SyntaxError("Unsupported file format") + msg = "Unsupported file format" + raise SyntaxError(msg) self.mode = "RGB" self._size = size @@ -162,7 +164,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("WMF save handler not installed") + msg = "WMF save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 4efedb77e..f0e05e867 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -49,7 +49,8 @@ class XVThumbImageFile(ImageFile.ImageFile): # check magic if not _accept(self.fp.read(6)): - raise SyntaxError("not an XV thumbnail file") + msg = "not an XV thumbnail file" + raise SyntaxError(msg) # Skip to beginning of next line self.fp.readline() @@ -58,7 +59,8 @@ class XVThumbImageFile(ImageFile.ImageFile): while True: s = self.fp.readline() if not s: - raise SyntaxError("Unexpected EOF reading XV thumbnail file") + msg = "Unexpected EOF reading XV thumbnail file" + raise SyntaxError(msg) if s[0] != 35: # ie. when not a comment: '#' break diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 59acabeba..ad18e0031 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -53,7 +53,8 @@ class XbmImageFile(ImageFile.ImageFile): m = xbm_head.match(self.fp.read(512)) if not m: - raise SyntaxError("not a XBM file") + msg = "not a XBM file" + raise SyntaxError(msg) xsize = int(m.group("width")) ysize = int(m.group("height")) @@ -70,7 +71,8 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): if im.mode != "1": - raise OSError(f"cannot write mode {im.mode} as XBM") + msg = f"cannot write mode {im.mode} as XBM" + raise OSError(msg) fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index aaed2039d..5fae4cd68 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -40,13 +40,15 @@ class XpmImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(9)): - raise SyntaxError("not an XPM file") + msg = "not an XPM file" + raise SyntaxError(msg) # skip forward to next string while True: s = self.fp.readline() if not s: - raise SyntaxError("broken XPM file") + msg = "broken XPM file" + raise SyntaxError(msg) m = xpm_head.match(s) if m: break @@ -57,7 +59,8 @@ class XpmImageFile(ImageFile.ImageFile): bpp = int(m.group(4)) if pal > 256 or bpp != 1: - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) # # load palette description @@ -91,13 +94,15 @@ class XpmImageFile(ImageFile.ImageFile): ) else: # unknown colour - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) break else: # missing colour key - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) self.mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 30a8a8971..7c4b1623d 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -43,14 +43,17 @@ def deprecate( if when is None: removed = "a future version" elif when <= int(__version__.split(".")[0]): - raise RuntimeError(f"{deprecated} {is_} deprecated and should be removed.") + msg = f"{deprecated} {is_} deprecated and should be removed." + raise RuntimeError(msg) elif when == 10: removed = "Pillow 10 (2023-07-01)" else: - raise ValueError(f"Unknown removal version, update {__name__}?") + msg = f"Unknown removal version, update {__name__}?" + raise ValueError(msg) if replacement and action: - raise ValueError("Use only one of 'replacement' and 'action'") + msg = "Use only one of 'replacement' and 'action'" + raise ValueError(msg) if replacement: action = f". Use {replacement} instead." diff --git a/src/PIL/features.py b/src/PIL/features.py index 3838568f3..6f9d99e76 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -25,7 +25,8 @@ def check_module(feature): :raises ValueError: If the module is not defined in this version of Pillow. """ if not (feature in modules): - raise ValueError(f"Unknown module {feature}") + msg = f"Unknown module {feature}" + raise ValueError(msg) module, ver = modules[feature] @@ -78,7 +79,8 @@ def check_codec(feature): :raises ValueError: If the codec is not defined in this version of Pillow. """ if feature not in codecs: - raise ValueError(f"Unknown codec {feature}") + msg = f"Unknown codec {feature}" + raise ValueError(msg) codec, lib = codecs[feature] @@ -135,7 +137,8 @@ def check_feature(feature): :raises ValueError: If the feature is not defined in this version of Pillow. """ if feature not in features: - raise ValueError(f"Unknown feature {feature}") + msg = f"Unknown feature {feature}" + raise ValueError(msg) module, flag, ver = features[feature] diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a061aaf17..68c2acd67 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -478,7 +478,8 @@ def extract_dep(url, filename): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: - raise RuntimeError("Attempted Path Traversal in Zip File") + msg = "Attempted Path Traversal in Zip File" + raise RuntimeError(msg) zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: @@ -486,7 +487,8 @@ def extract_dep(url, filename): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: - raise RuntimeError("Attempted Path Traversal in Tar File") + msg = "Attempted Path Traversal in Tar File" + raise RuntimeError(msg) tgz.extractall(sources_dir) else: raise RuntimeError("Unknown archive type: " + filename) @@ -642,9 +644,8 @@ if __name__ == "__main__": msvs = find_msvs() if msvs is None: - raise RuntimeError( - "Visual Studio not found. Please install Visual Studio 2017 or newer." - ) + msg = "Visual Studio not found. Please install Visual Studio 2017 or newer." + raise RuntimeError(msg) print("Found Visual Studio at:", msvs["vs_dir"]) print("Using output directory:", build_dir) From 68fdd2a9e76319f0021256a86d388df1a5f9875a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Dec 2022 14:24:28 +1100 Subject: [PATCH 028/220] Further improve exception traceback readability --- src/PIL/ImImagePlugin.py | 5 ++--- src/PIL/Image.py | 24 +++++++++++------------- src/PIL/ImageCms.py | 6 ++++-- src/PIL/ImageFile.py | 11 ++++++----- src/PIL/ImageMorph.py | 3 ++- src/PIL/PdfParser.py | 11 ++++++----- winbuild/build_prepare.py | 6 ++++-- 7 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index d0e9508fe..875a20326 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -201,9 +201,8 @@ class ImImageFile(ImageFile.ImageFile): else: - raise SyntaxError( - "Syntax error in IM header: " + s.decode("ascii", "replace") - ) + msg = "Syntax error in IM header: " + s.decode("ascii", "replace") + raise SyntaxError(msg) if not n: msg = "Not an IM file" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 386fb7c26..b22060965 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2129,7 +2129,7 @@ class Image: Resampling.BOX, Resampling.HAMMING, ): - message = f"Unknown resampling filter ({resample})." + msg = f"Unknown resampling filter ({resample})." filters = [ f"{filter[1]} ({filter[0]})" @@ -2142,9 +2142,8 @@ class Image: (Resampling.HAMMING, "Image.Resampling.HAMMING"), ) ] - raise ValueError( - message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] - ) + msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + raise ValueError(msg) if reducing_gap is not None and reducing_gap < 1.0: msg = "reducing_gap must be 1.0 or greater" @@ -2764,13 +2763,13 @@ class Image: Resampling.BICUBIC, ): if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): - message = { + msg = { Resampling.BOX: "Image.Resampling.BOX", Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.LANCZOS: "Image.Resampling.LANCZOS", }[resample] + f" ({resample}) cannot be used." else: - message = f"Unknown resampling filter ({resample})." + msg = f"Unknown resampling filter ({resample})." filters = [ f"{filter[1]} ({filter[0]})" @@ -2780,9 +2779,8 @@ class Image: (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), ) ] - raise ValueError( - message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] - ) + msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + raise ValueError(msg) image.load() @@ -3077,7 +3075,8 @@ def fromarray(obj, mode=None): try: mode, rawmode = _fromarray_typemap[typekey] except KeyError as e: - raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e + msg = "Cannot handle this data type: %s, %s" % typekey + raise TypeError(msg) from e else: rawmode = mode if mode in ["1", "L", "I", "P", "F"]: @@ -3276,9 +3275,8 @@ def open(fp, mode="r", formats=None): fp.close() for message in accept_warnings: warnings.warn(message) - raise UnidentifiedImageError( - "cannot identify image file %r" % (filename if filename else fp) - ) + msg = "cannot identify image file %r" % (filename if filename else fp) + raise UnidentifiedImageError(msg) # diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 2a2d372e5..f87849680 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -498,7 +498,8 @@ def buildTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -601,7 +602,8 @@ def buildProofTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0d3facf57..12391955f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -63,12 +63,13 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`, def raise_oserror(error): try: - message = Image.core.getcodecstatus(error) + msg = Image.core.getcodecstatus(error) except AttributeError: - message = ERRORS.get(error) - if not message: - message = f"decoder error {error}" - raise OSError(message + " when reading image file") + msg = ERRORS.get(error) + if not msg: + msg = f"decoder error {error}" + msg += " when reading image file" + raise OSError(msg) def _tilesort(t): diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 60cbbedc3..6fccc315b 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -146,7 +146,8 @@ class LutBuilder: for p in self.patterns: m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) if not m: - raise Exception('Syntax error in pattern "' + p + '"') + msg = 'Syntax error in pattern "' + p + '"' + raise Exception(msg) options = m.group(1) pattern = m.group(2) result = int(m.group(3)) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index e4a0f25a9..aa5ea2fbb 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -817,10 +817,10 @@ class PdfParser: try: stream_len = int(result[b"Length"]) except (TypeError, KeyError, ValueError) as e: - raise PdfFormatError( - "bad or missing Length in stream dict (%r)" - % result.get(b"Length", None) - ) from e + msg = "bad or missing Length in stream dict (%r)" % result.get( + b"Length", None + ) + raise PdfFormatError(msg) from e stream_data = data[m.end() : m.end() + stream_len] m = cls.re_stream_end.match(data, m.end() + stream_len) check_format_condition(m, "stream end not found") @@ -874,7 +874,8 @@ class PdfParser: if m: return cls.get_literal_string(data, m.end()) # return None, offset # fallback (only for debugging) - raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32])) + msg = "unrecognized object: " + repr(data[offset : offset + 32]) + raise PdfFormatError(msg) re_lit_str_token = re.compile( rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 68c2acd67..f5050946c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -491,7 +491,8 @@ def extract_dep(url, filename): raise RuntimeError(msg) tgz.extractall(sources_dir) else: - raise RuntimeError("Unknown archive type: " + filename) + msg = "Unknown archive type: " + filename + raise RuntimeError(msg) def write_script(name, lines): @@ -628,7 +629,8 @@ if __name__ == "__main__": elif arg == "--srcdir": sources_dir = os.path.sep + "src" else: - raise ValueError("Unknown parameter: " + arg) + msg = "Unknown parameter: " + arg + raise ValueError(msg) # dependency cache directory os.makedirs(depends_dir, exist_ok=True) From 91b01f4cc2b1728295c97ac4114f800637c5ead2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Dec 2022 16:48:33 +1100 Subject: [PATCH 029/220] Return from ImagingFill early if image has a zero dimension --- Tests/test_image.py | 5 +++++ src/libImaging/Fill.c | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index a37c90296..13c162812 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -512,6 +512,11 @@ class TestImage: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) + @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) + @pytest.mark.timeout(0.5) + def test_empty_image(self, size): + Image.new("RGB", size) + def test_storage_neg(self): # Storage.c accepted negative values for xsize, ysize. Was # test_neg_ppm, but the core function for that has been diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index f72060228..5b6bfb89c 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -24,6 +24,11 @@ ImagingFill(Imaging im, const void *colour) { int x, y; ImagingSectionCookie cookie; + /* 0-width or 0-height image. No need to do anything */ + if (!im->linesize || !im->ysize) { + return im; + } + if (im->type == IMAGING_TYPE_SPECIAL) { /* use generic API */ ImagingAccess access = ImagingAccessNew(im); From 907d59753bdd66460f0bc73e6022352f5ff14591 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 09:33:12 +1100 Subject: [PATCH 030/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4eebbda6a..904c73629 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Improve exception traceback readability #6836 + [hugovk, radarhere] + - Do not attempt to read IFD1 if absent #6840 [radarhere] From 559b7ae476d512165d851747f8c9da7df743a1b9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 09:43:36 +1100 Subject: [PATCH 031/220] Updated wording --- docs/deprecations.rst | 5 +++-- docs/releasenotes/9.4.0.rst | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a84a9fe36..4d48b822a 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -76,8 +76,9 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 .. note:: - Additional ``Image`` constants were deprecated in Pillow 9.1.0, but they - were later restored in Pillow 9.4.0. See :ref:`restored-image-constants` + Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that + was reversed in Pillow 9.4.0 and those constants will now remain available. + See :ref:`restored-image-constants` ===================================================== ============================================================ Deprecated Use instead diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 20a5afa63..2b111d5e4 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -115,8 +115,8 @@ format, known as "luminance" textures. Constants ^^^^^^^^^ -In Pillow 9.1.0, the following constants were deprecated. Those deprecations have now -been restored. +In Pillow 9.1.0, the following constants were deprecated. That has been reversed and +these constants will now remain available. - ``Image.NONE`` - ``Image.NEAREST`` From 2494e128ab099ee28601aad9ef2745f2dea15a41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 11:50:43 +1100 Subject: [PATCH 032/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 904c73629..df24f562f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830 + [radarhere] + - Improve exception traceback readability #6836 [hugovk, radarhere] From 280330476345c10a3c95ef44b8dba260c6694502 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 13:47:07 +1100 Subject: [PATCH 033/220] Skip timeout checks on slower running valgrind job --- .github/workflows/test-valgrind.yml | 2 +- Tests/test_file_pdf.py | 1 + Tests/test_image.py | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 219189cf2..f8b050f76 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -48,5 +48,5 @@ jobs: run: | # The Pillow user in the docker container is UID 1000 sudo chown -R 1000 $GITHUB_WORKSPACE - docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} sudo chown -R runner $GITHUB_WORKSPACE diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9667b6a4a..5299febe9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -286,6 +286,7 @@ def test_pdf_append_to_bytesio(): @pytest.mark.timeout(1) +@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) def test_redos(newline): malicious = b" trailer<<>>" + newline * 3456 diff --git a/Tests/test_image.py b/Tests/test_image.py index 13c162812..890769fcd 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -512,8 +512,11 @@ class TestImage: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) + @pytest.mark.timeout(0.75) + @pytest.mark.skipif( + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" + ) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - @pytest.mark.timeout(0.5) def test_empty_image(self, size): Image.new("RGB", size) From 13306974e749871822dac413be66e699a0f4645e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 20:14:17 +1100 Subject: [PATCH 034/220] Updated copyright year --- LICENSE | 2 +- docs/COPYING | 2 +- docs/conf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 40aabc323..616808a48 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2022 by Alex Clark and contributors + Copyright © 2010-2023 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source HPND License: diff --git a/docs/COPYING b/docs/COPYING index 25f03b343..b400381d3 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2022 by Alex Clark and contributors + Copyright © 2010-2023 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index 04823e2d7..fb58d25ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,7 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2022 Alex Clark and Contributors" +copyright = "1995-2011 Fredrik Lundh, 2010-2023 Alex Clark and Contributors" author = "Fredrik Lundh, Alex Clark and Contributors" # The version info for the project you're documenting, acts as replacement for From 7f1708415c23241ef86f2509418a2558e6990320 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 22:24:58 +1100 Subject: [PATCH 035/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index df24f562f..655089ab2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Return from ImagingFill early if image has a zero dimension #6842 + [radarhere] + - Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830 [radarhere] From 87d1770c182bb3e19229f1a652cabedce29b891e Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Wed, 2 Nov 2022 23:11:57 +0100 Subject: [PATCH 036/220] Fix null pointer dereference crash with malformed font --- Tests/fonts/fuzz_font-5203009437302784 | 10 ++++++++++ Tests/test_font_crash.py | 21 +++++++++++++++++++++ src/_imagingft.c | 6 ++++++ 3 files changed, 37 insertions(+) create mode 100644 Tests/fonts/fuzz_font-5203009437302784 create mode 100644 Tests/test_font_crash.py diff --git a/Tests/fonts/fuzz_font-5203009437302784 b/Tests/fonts/fuzz_font-5203009437302784 new file mode 100644 index 000000000..0465e48c2 --- /dev/null +++ b/Tests/fonts/fuzz_font-5203009437302784 @@ -0,0 +1,10 @@ +STARTFONT +FONT ÿ +SIZE 10 +FONTBOUNDINGBOX +CHARS +STARTCHAR +ENCODING +BBX 2 5 +ENDCHAR +ENDFONT diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py new file mode 100644 index 000000000..020ddfcd9 --- /dev/null +++ b/Tests/test_font_crash.py @@ -0,0 +1,21 @@ +from PIL import Image, ImageDraw, ImageFont + +import pytest + +from .helper import skip_unless_feature + +class TestFontCrash: + def _fuzz_font(self, font): + # from fuzzers.fuzz_font + font.getbbox("ABC") + font.getmask("test text") + with Image.new(mode="RGBA", size=(200, 200)) as im: + draw = ImageDraw.Draw(im) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "Test Text", font=font, fill="#000") + + @skip_unless_feature("freetype2") + def test_segfault(self): + with pytest.raises(OSError): + font= ImageFont.truetype('Tests/fonts/fuzz_font-5203009437302784') + self._fuzz_font(font) diff --git a/src/_imagingft.c b/src/_imagingft.c index b52d6353e..319098897 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -921,6 +921,12 @@ font_render(FontObject *self, PyObject *args) { yy = -(py + glyph_slot->bitmap_top); } + // Null buffer, is dereferenced in FT_Bitmap_Convert + if (!bitmap.buffer && bitmap.rows) { + return geterror(0x9D); // Bitmap missing + goto glyph_error; + } + /* convert non-8bpp bitmaps */ switch (bitmap.pixel_mode) { case FT_PIXEL_MODE_MONO: From f2b36a1833f1b95d7a8d336432170cad091c6236 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Nov 2022 09:18:47 +1100 Subject: [PATCH 037/220] Lint fixes --- Tests/test_font_crash.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 020ddfcd9..e8d612a7f 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -4,6 +4,7 @@ import pytest from .helper import skip_unless_feature + class TestFontCrash: def _fuzz_font(self, font): # from fuzzers.fuzz_font @@ -17,5 +18,5 @@ class TestFontCrash: @skip_unless_feature("freetype2") def test_segfault(self): with pytest.raises(OSError): - font= ImageFont.truetype('Tests/fonts/fuzz_font-5203009437302784') + font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) From 1c57ab84294845a49b4f5dc2b2444a8eaff70110 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 3 Nov 2022 21:26:17 +0100 Subject: [PATCH 038/220] Return a PyError instead of a fake fterror. * Update Tests to IOError rather than OSError --- Tests/oss-fuzz/test_fuzzers.py | 4 +++- Tests/test_font_crash.py | 2 +- src/_imagingft.c | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 629e9ac00..1b0b1d3dc 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -57,6 +57,8 @@ def test_fuzz_fonts(path): with open(path, "rb") as f: try: fuzzers.fuzz_font(f.read()) - except (Image.DecompressionBombError, Image.DecompressionBombWarning): + except (Image.DecompressionBombError, + Image.DecompressionBombWarning, + IOError): pass assert True diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index e8d612a7f..9a2110c0c 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -17,6 +17,6 @@ class TestFontCrash: @skip_unless_feature("freetype2") def test_segfault(self): - with pytest.raises(OSError): + with pytest.raises(IOError): font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) diff --git a/src/_imagingft.c b/src/_imagingft.c index 319098897..053ef1e7d 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -923,7 +923,7 @@ font_render(FontObject *self, PyObject *args) { // Null buffer, is dereferenced in FT_Bitmap_Convert if (!bitmap.buffer && bitmap.rows) { - return geterror(0x9D); // Bitmap missing + PyErr_SetString(PyExc_IOError, "Bitmap missing for glyph"); goto glyph_error; } From 51d95add6a306ea9dc1b0e2dacc202f69e4565e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 15:41:17 +1100 Subject: [PATCH 039/220] Replaced IOError with OSError --- Tests/oss-fuzz/test_fuzzers.py | 2 +- Tests/test_font_crash.py | 2 +- src/_imagingft.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 1b0b1d3dc..fb8f87e86 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -59,6 +59,6 @@ def test_fuzz_fonts(path): fuzzers.fuzz_font(f.read()) except (Image.DecompressionBombError, Image.DecompressionBombWarning, - IOError): + OSError): pass assert True diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 9a2110c0c..e8d612a7f 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -17,6 +17,6 @@ class TestFontCrash: @skip_unless_feature("freetype2") def test_segfault(self): - with pytest.raises(IOError): + with pytest.raises(OSError): font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) diff --git a/src/_imagingft.c b/src/_imagingft.c index 053ef1e7d..0db17a5a6 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -923,7 +923,7 @@ font_render(FontObject *self, PyObject *args) { // Null buffer, is dereferenced in FT_Bitmap_Convert if (!bitmap.buffer && bitmap.rows) { - PyErr_SetString(PyExc_IOError, "Bitmap missing for glyph"); + PyErr_SetString(PyExc_OSError, "Bitmap missing for glyph"); goto glyph_error; } From c977526cfeda89e86d0144f5f8dca06cd05dbef5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 17:45:09 +1100 Subject: [PATCH 040/220] Lint fixes --- Tests/oss-fuzz/test_fuzzers.py | 4 +--- Tests/test_font_crash.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index fb8f87e86..dc111c38b 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -57,8 +57,6 @@ def test_fuzz_fonts(path): with open(path, "rb") as f: try: fuzzers.fuzz_font(f.read()) - except (Image.DecompressionBombError, - Image.DecompressionBombWarning, - OSError): + except (Image.DecompressionBombError, Image.DecompressionBombWarning, OSError): pass assert True diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index e8d612a7f..27663f396 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,7 +1,7 @@ -from PIL import Image, ImageDraw, ImageFont - import pytest +from PIL import Image, ImageDraw, ImageFont + from .helper import skip_unless_feature From 009bbe25ecbcb14f4e238089f11915d01dfcf1b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 1 Jan 2023 23:26:00 +1100 Subject: [PATCH 041/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 655089ab2..e6a494674 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed null pointer dereference crash with malformed font #6846 + [wiredfool, radarhere] + - Return from ImagingFill early if image has a zero dimension #6842 [radarhere] From a632b7a3e71a0122caa9be27fb0b1701ffb49e26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 00:25:49 +1100 Subject: [PATCH 042/220] Added release notes for #6842 --- docs/releasenotes/9.4.0.rst | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 2b111d5e4..a0d26dc52 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,30 +1,6 @@ 9.4.0 ----- -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -TODO - -Deprecations -============ - -TODO -^^^^ - -TODO - -API Changes -=========== - -TODO -^^^^ - -TODO - API Additions ============= @@ -96,10 +72,14 @@ When saving a JPEG image, a comment can now be written from Security ======== -TODO -^^^^ +Fix memory DOS in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +A corrupt or specially crafted TTF font could have font metrics that lead to +unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not +check the image size before allocating memory for it. This dates to the PIL +fork. Pilllow 8.2.0 added a check for large sizes, but did not consider the +case where one dimension was zero. Other Changes ============= From 35b4c433b33da3fa1e9a3193809c3fd7ec58d042 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 00:32:35 +1100 Subject: [PATCH 043/220] Added release notes for #6846 --- docs/releasenotes/9.4.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index a0d26dc52..2d83b7bf5 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -81,6 +81,13 @@ check the image size before allocating memory for it. This dates to the PIL fork. Pilllow 8.2.0 added a check for large sizes, but did not consider the case where one dimension was zero. +Null pointer dereference crash in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a +crash. An error is now raised instead. This would have been present since +Pillow 8.0.0. + Other Changes ============= From e908afea40ec54c43954c9a70be78af670dfb442 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 2 Jan 2023 08:17:47 +1100 Subject: [PATCH 044/220] Updated security descriptions Co-authored-by: Hugo van Kemenade --- docs/releasenotes/9.4.0.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 2d83b7bf5..0af5bc8ca 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -78,14 +78,14 @@ Fix memory DOS in ImageFont A corrupt or specially crafted TTF font could have font metrics that lead to unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check the image size before allocating memory for it. This dates to the PIL -fork. Pilllow 8.2.0 added a check for large sizes, but did not consider the -case where one dimension was zero. +fork. Pillow 8.2.0 added a check for large sizes, but did not consider the +case where one dimension is zero. Null pointer dereference crash in ImageFont ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a -crash. An error is now raised instead. This would have been present since +crash. An error is now raised instead. This has been present since Pillow 8.0.0. Other Changes From d4d981dc9ff923a099f0e5be95eb9a2449b74f35 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 08:41:50 +1100 Subject: [PATCH 045/220] Updated size parameter descriptions --- src/PIL/Image.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6c6b07d61..4e1c3a021 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1271,7 +1271,8 @@ class Image: currently implemented only for JPEG and MPO images. :param mode: The requested mode. - :param size: The requested size. + :param size: The requested size in pixels, as a 2-tuple: + (width, height). """ pass @@ -2551,7 +2552,8 @@ class Image: apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original image. - :param size: Requested size. + :param size: The requested size in pixels, as a 2-tuple: + (width, height). :param resample: Optional resampling filter. This can be one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, @@ -2638,7 +2640,8 @@ class Image: given size, and the same mode as the original, and copies data to the new image using the given transform. - :param size: The output size. + :param size: The output size in pixels, as a 2-tuple: + (width, height). :param method: The transformation method. This is one of :py:data:`Transform.EXTENT` (cut out a rectangular subregion), :py:data:`Transform.AFFINE` (affine transform), From a5bbab1c1e63b439de191ef2040173713b26d2da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 10:29:07 +1100 Subject: [PATCH 046/220] 9.4.0 version bump --- CHANGES.rst | 2 +- src/PIL/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e6a494674..7ec7b936d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changelog (Pillow) ================== -9.4.0 (unreleased) +9.4.0 (2023-01-02) ------------------ - Fixed null pointer dereference crash with malformed font #6846 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 1cc1d0f1c..aca0aba02 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.4.0.dev0" +__version__ = "9.4.0" From 549560cf553d00c6e06a4c7e27fba56f0aba1c41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 14:03:31 +1100 Subject: [PATCH 047/220] 9.5.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index aca0aba02..7baa9fb6c 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.4.0" +__version__ = "9.5.0.dev0" From 97385d7cc7e983c7c1a22bf777152f5ce1dc917c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 19:54:12 +1100 Subject: [PATCH 048/220] Relaxed child images check to allow for libjpeg --- Tests/test_file_jpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..fb8954125 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -447,7 +447,7 @@ class TestFileJpeg: ims = im.get_child_images() assert len(ims) == 1 - assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: From 6c30b2c00d061f5b634f8c10337eb8a7fe29192d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 21:03:45 +1100 Subject: [PATCH 049/220] arr.tobytes() always exists in Python >= 3.2 --- Tests/test_imagepath.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index de3920cf5..861fb64f0 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -58,10 +58,7 @@ def test_path(): assert list(p) == [(0.0, 1.0)] arr = array.array("f", [0, 1]) - if hasattr(arr, "tobytes"): - p = ImagePath.Path(arr.tobytes()) - else: - p = ImagePath.Path(arr.tostring()) + p = ImagePath.Path(arr.tobytes()) assert list(p) == [(0.0, 1.0)] From 9342f9a0e67cf5dacf5fee06b1c806a99a68e1fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 18:34:37 +0000 Subject: [PATCH 050/220] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/isort: 5.11.1 → 5.11.4](https://github.com/PyCQA/isort/compare/5.11.1...5.11.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d019d3e7f..d790e7850 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/PyCQA/isort - rev: 5.11.1 + rev: 5.11.4 hooks: - id: isort From ea9a1b84aa5a8f6ab077974a052e54ff52ee2c50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Jan 2023 14:09:45 +1100 Subject: [PATCH 051/220] LOAD_TRUNCATED_IMAGES may allow PNG images to open --- src/PIL/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index e65b155b2..4b76e893f 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -75,6 +75,10 @@ _plugins = [ class UnidentifiedImageError(OSError): """ Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. + + If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` + to true may allow the image to be opened after all. The setting will ignore missing + data and checksum failures. """ pass From e653aaee899e87b2b886251538c4eaa7b593e8b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jan 2023 00:43:48 +1100 Subject: [PATCH 052/220] NotImplementedError will not be raised if xclip is available --- Tests/test_imagegrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 317db4c01..fa88065f4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -64,7 +64,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 ) p.communicate() else: - if not shutil.which("wl-paste"): + if not shutil.which("wl-paste") and not shutil.which("xclip"): with pytest.raises( NotImplementedError, match="wl-paste or xclip is required for" From 2d6f9c16fcb6d5474833a301629f43818d5a8aac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jan 2023 14:06:47 +1100 Subject: [PATCH 053/220] Announce releases on Fosstodon [ci skip] --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index b05067484..27c21be87 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -111,7 +111,7 @@ Released as needed privately to individual vendors for critical security-related ## Publicize Release -* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Fosstodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 ## Documentation From b6a9fccd87faf03d140030aa9f654af0ea10d520 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jan 2023 14:44:54 +1100 Subject: [PATCH 054/220] Added Fosstodon badge [ci skip] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8ee68f9b8..9f549ece6 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,9 @@ As of 2019, Pillow development is Follow on https://twitter.com/PythonPillow + Follow on https://fosstodon.org/@pillow From fc84d6e37f1e9e2bff64f93c17e35eec28d9d01f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jan 2023 14:59:10 +1100 Subject: [PATCH 055/220] Added Fosstodon URL to setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index b562e2934..2dc552a2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ project_urls = Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst Twitter=https://twitter.com/PythonPillow + Mastodon=https://fosstodon.org/@pillow [options] packages = PIL From e82f545ed0c0321556ab1bd2e924a5cb03fe6b27 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 5 Jan 2023 09:56:59 +1100 Subject: [PATCH 056/220] Refer to Mastodon [ci skip] Co-authored-by: Hugo van Kemenade --- README.md | 3 ++- RELEASING.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f549ece6..dd1d2c60f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ As of 2019, Pillow development is src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"> Follow on https://fosstodon.org/@pillow + src="https://img.shields.io/badge/publish-on%20Mastodon-595aff" + rel="me"> diff --git a/RELEASING.md b/RELEASING.md index 27c21be87..c203a9c12 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -111,7 +111,7 @@ Released as needed privately to individual vendors for critical security-related ## Publicize Release -* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Fosstodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 ## Documentation From 0421b2f2a04e7ab17c866735c2a6b3257f65045b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Jan 2023 10:43:01 +1100 Subject: [PATCH 057/220] Added social links to docs --- README.md | 2 +- docs/index.rst | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd1d2c60f..489d3db54 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ As of 2019, Pillow development is src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"> Follow on https://fosstodon.org/@pillow diff --git a/docs/index.rst b/docs/index.rst index 5bcd5afa5..674b31bd7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,6 +73,18 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Thu, 5 Jan 2023 00:04:50 +0000 Subject: [PATCH 058/220] Fix tcl/tk loading error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej BaranoviÄ --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 506bb7008..6ad3aaba1 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -368,7 +368,7 @@ load_tkinter_funcs(void) { } else if (found_tk != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); } - return (int) ((found_tcl != 1) && (found_tk != 1)); + return (int) ((found_tcl != 1) || (found_tk != 1)); } #else /* not Windows */ From da39e4e38e866e715eccf29feb23a444ab23f60b Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Thu, 5 Jan 2023 00:05:36 +0000 Subject: [PATCH 059/220] Fix tcl/tk loading error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej BaranoviÄ --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 6ad3aaba1..8225756ba 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -365,7 +365,7 @@ load_tkinter_funcs(void) { free(hMods); if (found_tcl != 1) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); - } else if (found_tk != 1) { + } else if (found_tk == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); } return (int) ((found_tcl != 1) || (found_tk != 1)); From 2cc40cc7f4a7d51b29c66c0da4d785a5d5920c3c Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Thu, 5 Jan 2023 00:05:54 +0000 Subject: [PATCH 060/220] Fix tcl/tk loading error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej BaranoviÄ --- src/Tk/tkImaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 8225756ba..ad503baec 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -363,7 +363,7 @@ load_tkinter_funcs(void) { } free(hMods); - if (found_tcl != 1) { + if (found_tcl == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tcl routines"); } else if (found_tk == 0) { PyErr_SetString(PyExc_RuntimeError, "Could not find Tk routines"); From ea83ebbcf90ffff7b68ffc9ce90aad5c14c10f05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Jan 2023 20:09:47 +1100 Subject: [PATCH 061/220] 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 d3d7566d9a4d63415fa4fc76864f95451f289725 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jan 2023 11:27:43 +1100 Subject: [PATCH 062/220] Refer to Resampling enum --- docs/handbook/concepts.rst | 50 ++++++++++++------------ docs/reference/Image.rst | 1 + docs/releasenotes/2.7.0.rst | 76 +++++++++++++++++-------------------- src/PIL/ImageOps.py | 12 ++++-- 4 files changed, 69 insertions(+), 70 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 01f75e9a3..45c662bd6 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -148,44 +148,44 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. py:currentmodule:: PIL.Image -.. data:: NEAREST +.. data:: Resampling.NEAREST Pick one nearest pixel from the input image. Ignore all other input pixels. -.. data:: BOX +.. data:: Resampling.BOX Each pixel of source image contributes to one pixel of the destination image with identical weights. - For upscaling is equivalent of :data:`NEAREST`. + For upscaling is equivalent of :data:`Resampling.NEAREST`. This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` methods. .. versionadded:: 3.4.0 -.. data:: BILINEAR +.. data:: Resampling.BILINEAR For resize calculate the output pixel value using linear interpolation on all pixels that may contribute to the output value. For other transformations linear interpolation over a 2x2 environment in the input image is used. -.. data:: HAMMING +.. data:: Resampling.HAMMING - Produces a sharper image than :data:`BILINEAR`, doesn't have dislocations - on local level like with :data:`BOX`. + Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have + dislocations on local level like with :data:`Resampling.BOX`. This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` methods. .. versionadded:: 3.4.0 -.. data:: BICUBIC +.. data:: Resampling.BICUBIC For resize calculate the output pixel value using cubic interpolation on all pixels that may contribute to the output value. For other transformations cubic interpolation over a 4x4 environment in the input image is used. -.. data:: LANCZOS +.. data:: Resampling.LANCZOS Calculate the output pixel value using a high-quality Lanczos filter (a truncated sinc) on all pixels that may contribute to the output value. @@ -198,19 +198,19 @@ pixel, the Python Imaging Library provides different resampling *filters*. Filters comparison table ~~~~~~~~~~~~~~~~~~~~~~~~ -+----------------+-------------+-----------+-------------+ -| Filter | Downscaling | Upscaling | Performance | -| | quality | quality | | -+================+=============+===========+=============+ -|:data:`NEAREST` | | | â­â­â­â­â­ | -+----------------+-------------+-----------+-------------+ -|:data:`BOX` | â­ | | â­â­â­â­ | -+----------------+-------------+-----------+-------------+ -|:data:`BILINEAR`| â­ | â­ | â­â­â­ | -+----------------+-------------+-----------+-------------+ -|:data:`HAMMING` | â­â­ | | â­â­â­ | -+----------------+-------------+-----------+-------------+ -|:data:`BICUBIC` | â­â­â­ | â­â­â­ | â­â­ | -+----------------+-------------+-----------+-------------+ -|:data:`LANCZOS` | â­â­â­â­ | â­â­â­â­ | â­ | -+----------------+-------------+-----------+-------------+ ++---------------------------+-------------+-----------+-------------+ +| Filter | Downscaling | Upscaling | Performance | +| | quality | quality | | ++===========================+=============+===========+=============+ +|:data:`Resampling.NEAREST` | | | â­â­â­â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BOX` | â­ | | â­â­â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BILINEAR`| â­ | â­ | â­â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.HAMMING` | â­â­ | | â­â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.BICUBIC` | â­â­â­ | â­â­â­ | â­â­ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.LANCZOS` | â­â­â­â­ | â­â­â­â­ | â­ | ++---------------------------+-------------+-----------+-------------+ diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 7f6f666c3..ad0abbbd9 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -430,6 +430,7 @@ See :ref:`concept-filters` for details. .. autoclass:: Resampling :members: :undoc-members: + :noindex: Some deprecated filters are also available under the following names: diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index dda814c1f..0b3eeeb49 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -29,84 +29,78 @@ Image resizing filters Image resizing methods :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells which filter should be used for resampling. Possible values are: -:py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BILINEAR`, -:py:data:`PIL.Image.BICUBIC` and :py:data:`PIL.Image.ANTIALIAS`. -Almost all of them were changed in this version. +``NEAREST``, ``BILINEAR``, ``BICUBIC`` and ``ANTIALIAS``. Almost all of them +were changed in this version. Bicubic and bilinear downscaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -From the beginning :py:data:`~PIL.Image.BILINEAR` and -:py:data:`~PIL.Image.BICUBIC` filters were based on affine transformations -and used a fixed number of pixels from the source image for every destination -pixel (2x2 pixels for :py:data:`~PIL.Image.BILINEAR` and 4x4 for -:py:data:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for -downscaling. At the same time, a high quality convolutions-based algorithm with -flexible kernel was used for :py:data:`~PIL.Image.ANTIALIAS` filter. +From the beginning ``BILINEAR`` and ``BICUBIC`` filters were based on affine +transformations and used a fixed number of pixels from the source image for +every destination pixel (2x2 pixels for ``BILINEAR`` and 4x4 for ``BICUBIC``). +This gave an unsatisfactory result for downscaling. At the same time, a high +quality convolutions-based algorithm with flexible kernel was used for +``ANTIALIAS`` filter. Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used for all of these three filters. If you have previously used any tricks to maintain quality when downscaling with -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters -(for example, reducing within several steps), they are unnecessary now. +``BILINEAR`` and ``BICUBIC`` filters (for example, reducing within several +steps), they are unnecessary now. Antialias renamed to Lanczos ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A new :py:data:`PIL.Image.LANCZOS` constant was added instead of -:py:data:`~PIL.Image.ANTIALIAS`. +A new ``LANCZOS`` constant was added instead of ``ANTIALIAS``. -When :py:data:`~PIL.Image.ANTIALIAS` was initially added, it was the only -high-quality filter based on convolutions. It's name was supposed to reflect -this. Starting from Pillow 2.7.0 all resize method are based on convolutions. -All of them are antialias from now on. And the real name of the -:py:data:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. +When ``ANTIALIAS`` was initially added, it was the only high-quality filter +based on convolutions. It's name was supposed to reflect this. Starting from +Pillow 2.7.0 all resize method are based on convolutions. All of them are +antialias from now on. And the real name of the ``ANTIALIAS`` filter is Lanczos +filter. -The :py:data:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility -and is an alias for :py:data:`~PIL.Image.LANCZOS`. +The ``ANTIALIAS`` constant is left for backward compatibility and is an alias +for ``LANCZOS``. Lanczos upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The image upscaling quality with :py:data:`~PIL.Image.LANCZOS` filter was -almost the same as :py:data:`~PIL.Image.BILINEAR` due to bug. This has been fixed. +The image upscaling quality with ``LANCZOS`` filter was almost the same as +``BILINEAR`` due to a bug. This has been fixed. Bicubic upscaling quality ^^^^^^^^^^^^^^^^^^^^^^^^^ -The :py:data:`~PIL.Image.BICUBIC` filter for affine transformations produced -sharp, slightly pixelated image for upscaling. Bicubic for convolutions is -more soft. +The ``BICUBIC`` filter for affine transformations produced sharp, slightly +pixelated image for upscaling. Bicubic for convolutions is more soft. Resize performance ^^^^^^^^^^^^^^^^^^ In most cases, convolution is more a expensive algorithm for downscaling because it takes into account all the pixels of source image. Therefore -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters' -performance can be lower than before. On the other hand the quality of -:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` was close to -:py:data:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks -you can switch to :py:data:`~PIL.Image.NEAREST` filter for downscaling, -which will give a huge improvement in performance. +``BILINEAR`` and ``BICUBIC`` filters' performance can be lower than before. +On the other hand the quality of ``BILINEAR`` and ``BICUBIC`` was close to +``NEAREST``. So if such quality is suitable for your tasks you can switch to +``NEAREST`` filter for downscaling, which will give a huge improvement in +performance. At the same time performance of convolution resampling for downscaling has been improved by around a factor of two compared to the previous version. -The upscaling performance of the :py:data:`~PIL.Image.LANCZOS` filter has -remained the same. For :py:data:`~PIL.Image.BILINEAR` filter it has improved by -1.5 times and for :py:data:`~PIL.Image.BICUBIC` by four times. +The upscaling performance of the ``LANCZOS`` filter has remained the same. For +``BILINEAR`` filter it has improved by 1.5 times and for ``BICUBIC`` by four +times. Default filter for thumbnails ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was -changed from :py:data:`~PIL.Image.NEAREST` to :py:data:`~PIL.Image.ANTIALIAS`. -Antialias was chosen because all the other filters gave poor quality for -reduction. Starting from Pillow 2.7.0, :py:data:`~PIL.Image.ANTIALIAS` has been -replaced with :py:data:`~PIL.Image.BICUBIC`, because it's faster and -:py:data:`~PIL.Image.ANTIALIAS` doesn't give any advantages after -downscaling with libjpeg, which uses supersampling internally, not convolutions. +changed from ``NEAREST`` to ``ANTIALIAS``. Antialias was chosen because all the +other filters gave poor quality for reduction. Starting from Pillow 2.7.0, +``ANTIALIAS`` has been replaced with ``BICUBIC``, because it's faster and +``ANTIALIAS`` doesn't give any advantages after downscaling with libjpeg, which +uses supersampling internally, not convolutions. Image transposition ------------------- diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index e2168ce62..e7719fcf9 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -248,7 +248,8 @@ def contain(image, size, method=Image.Resampling.BICUBIC): :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :return: An image. """ @@ -276,7 +277,8 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :param color: The background color of the padded image. :param centering: Control the position of the original image within the padded version. @@ -328,7 +330,8 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): :param image: The image to rescale. :param factor: The expansion factor, as a float. :param resample: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ if factor == 1: @@ -425,7 +428,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :py:attr:`PIL.Image.Resampling.BICUBIC`. + See :ref:`concept-filters`. :param bleed: Remove a border around the outside of the image from all four edges. The value is a decimal percentage (use 0.01 for one percent). The default value is 0 (no border). From 86634b835257a7d3438eaf783d70eca12e20b13e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Jan 2023 17:53:21 +1100 Subject: [PATCH 063/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7ec7b936d..9a267bc9c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +9.5.0 (unreleased) +------------------ + +- Support arbitrary number of loaded modules on Windows #6761 + [javidcf, radarhere, nulano] + 9.4.0 (2023-01-02) ------------------ From 52ed578947c8715aeaa83f34d299c73cb4db74ac Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 8 Oct 2022 17:14:11 -0500 Subject: [PATCH 064/220] add extra variable so linter doesn't split line --- Tests/test_pdfparser.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index ea9b33dfc..43e244c7b 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -88,9 +88,8 @@ def test_parsing(): b"D:20180729214124+08'00'": "20180729134124", b"D:20180729214124-05'00'": "20180730024124", }.items(): - d = PdfParser.get_value(b"<>", 0)[ - 0 - ] + b = b"<>" + d = PdfParser.get_value(b, 0)[0] assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value From 4e6e69aafb9b0aabb98c42aab030b5b2254d302a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 9 Oct 2022 02:55:01 -0500 Subject: [PATCH 065/220] remove loop left from before parametrization --- Tests/test_image_resample.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 53ceb6df0..be49955dd 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -135,16 +135,15 @@ class TestImagingCoreResampleAccuracy: @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) def test_reduce_bicubic(self, mode): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (12, 12), 0xE1) - case = case.resize((6, 6), Image.Resampling.BICUBIC) - # fmt: off - data = ("e1 e3 d4" - "e3 e5 d6" - "d4 d6 c9") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (6, 6))) + case = self.make_case(mode, (12, 12), 0xE1) + case = case.resize((6, 6), Image.Resampling.BICUBIC) + # fmt: off + data = ("e1 e3 d4" + "e3 e5 d6" + "d4 d6 c9") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) def test_reduce_lanczos(self, mode): From 48b6d4fd60d2f792ae7ee203aea686effd9617fd Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 9 Oct 2022 02:57:15 -0500 Subject: [PATCH 066/220] remove no-format tags and fix comment locations --- Tests/test_image_transform.py | 50 +++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index a78349801..7411f0b78 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -42,12 +42,12 @@ class TestImageTransform: def test_extent(self): im = hopper("RGB") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.EXTENT, - (0, 0, - w//2, h//2), # ul -> lr - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.EXTENT, + (0, 0, w // 2, h // 2), # ul -> lr + Image.Resampling.BILINEAR, + ) scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) @@ -58,13 +58,12 @@ class TestImageTransform: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.QUAD, - (0, 0, 0, h//2, - # ul -> ccw around quad: - w//2, h//2, w//2, 0), - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.QUAD, + (0, 0, 0, h // 2, w // 2, h // 2, w // 2, 0), # ul -> ccw around quad + Image.Resampling.BILINEAR, + ) scaled = im.transform( (w, h), @@ -99,16 +98,21 @@ class TestImageTransform: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") (w, h) = im.size - # fmt: off - transformed = im.transform(im.size, Image.Transform.MESH, - [((0, 0, w//2, h//2), # box - (0, 0, 0, h, - w, h, w, 0)), # ul -> ccw around quad - ((w//2, h//2, w, h), # box - (0, 0, 0, h, - w, h, w, 0))], # ul -> ccw around quad - Image.Resampling.BILINEAR) - # fmt: on + transformed = im.transform( + im.size, + Image.Transform.MESH, + ( + ( + (0, 0, w // 2, h // 2), # box + (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad + ), + ( + (w // 2, h // 2, w, h), # box + (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad + ), + ), + Image.Resampling.BILINEAR, + ) scaled = im.transform( (w // 2, h // 2), From 04199b6066aedbdef961008fadcc726d0546a0e1 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 9 Oct 2022 02:58:14 -0500 Subject: [PATCH 067/220] sort colors before comparing them --- Tests/test_image_transform.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 7411f0b78..64a5c9459 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -178,11 +178,13 @@ class TestImageTransform: im = op(im, (40, 10)) - colors = im.getcolors() - assert colors == [ - (20 * 10, opaque), - (20 * 10, transparent), - ] + colors = sorted(im.getcolors()) + assert colors == sorted( + ( + (20 * 10, opaque), + (20 * 10, transparent), + ) + ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) def test_nearest_resize(self, mode): From 246f6fa46a387e554cb608c87d6086b8cd368e36 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 8 Jan 2023 04:45:16 +1100 Subject: [PATCH 068/220] Simply attribute reference Co-authored-by: Hugo van Kemenade --- src/PIL/ImageOps.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index e7719fcf9..16c83f4e4 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -248,7 +248,7 @@ def contain(image, size, method=Image.Resampling.BICUBIC): :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.Resampling.BICUBIC`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. See :ref:`concept-filters`. :return: An image. """ @@ -277,7 +277,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.Resampling.BICUBIC`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. See :ref:`concept-filters`. :param color: The background color of the padded image. :param centering: Control the position of the original image within the @@ -330,7 +330,7 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): :param image: The image to rescale. :param factor: The expansion factor, as a float. :param resample: Resampling method to use. Default is - :py:attr:`PIL.Image.Resampling.BICUBIC`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -428,7 +428,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, :param size: The requested output size in pixels, given as a (width, height) tuple. :param method: Resampling method to use. Default is - :py:attr:`PIL.Image.Resampling.BICUBIC`. + :py:attr:`~PIL.Image.Resampling.BICUBIC`. See :ref:`concept-filters`. :param bleed: Remove a border around the outside of the image from all four edges. The value is a decimal percentage (use 0.01 for From b2b8c833aaf2778defea86b99e3c12c1198ffd64 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 7 Jan 2023 20:25:50 +0200 Subject: [PATCH 069/220] Use single isinstance call for multiple types --- src/PIL/PdfParser.py | 4 +--- winbuild/build_prepare.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index aa5ea2fbb..1b3cb52a2 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -328,9 +328,7 @@ def pdf_repr(x): return b"null" elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): return bytes(x) - elif isinstance(x, int): - return str(x).encode("us-ascii") - elif isinstance(x, float): + elif isinstance(x, (int, float)): return str(x).encode("us-ascii") elif isinstance(x, time.struct_time): return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f5050946c..df39260e0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -39,7 +39,7 @@ def cmd_rmdir(path): def cmd_nmake(makefile=None, target="", params=None): if params is None: params = "" - elif isinstance(params, list) or isinstance(params, tuple): + elif isinstance(params, (list, tuple)): params = " ".join(params) else: params = str(params) @@ -58,7 +58,7 @@ def cmd_nmake(makefile=None, target="", params=None): def cmd_cmake(params=None, file="."): if params is None: params = "" - elif isinstance(params, list) or isinstance(params, tuple): + elif isinstance(params, (list, tuple)): params = " ".join(params) else: params = str(params) From 2df4865e427a7d4dfc288ffe87d0b40c402b1375 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 7 Jan 2023 20:36:17 +0200 Subject: [PATCH 070/220] Use 'key in mydict' instead of 'key in mydict.keys()' --- Tests/test_file_png.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/TiffImagePlugin.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 9481cd5dd..133f3e47e 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -593,7 +593,7 @@ class TestFilePng: def test_textual_chunks_after_idat(self): with Image.open("Tests/images/hopper.png") as im: - assert "comment" in im.text.keys() + assert "comment" in im.text for k, v in { "date:create": "2014-09-04T09:37:08+03:00", "date:modify": "2014-09-04T09:37:08+03:00", diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d01315b20..6ee1bd3d8 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -487,7 +487,7 @@ def _normalize_mode(im): if Image.getmodebase(im.mode) == "RGB": im = im.convert("P", palette=Image.Palette.ADAPTIVE) if im.palette.mode == "RGBA": - for rgba in im.palette.colors.keys(): + for rgba in im.palette.colors: if rgba[3] == 0: im.info["transparency"] = im.palette.colors[rgba] break diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4e1c3a021..b0ff5173c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3841,7 +3841,7 @@ class Exif(MutableMapping): def __str__(self): if self._info is not None: # Load all keys into self._data - for tag in self._info.keys(): + for tag in self._info: self[tag] return str(self._data) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 431edfd9b..431a95701 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -257,7 +257,7 @@ OPEN_INFO = { (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } -MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys()) +MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) PREFIXES = [ b"MM\x00\x2A", # Valid TIFF header with big-endian byte order @@ -1222,7 +1222,7 @@ class TiffImageFile(ImageFile.ImageFile): # load IFD data from fp before it is closed exif = self.getexif() - for key in TiffTags.TAGS_V2_GROUPS.keys(): + for key in TiffTags.TAGS_V2_GROUPS: if key not in exif: continue exif.get_ifd(key) @@ -1629,7 +1629,7 @@ def _save(im, fp, filename): if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: - if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys(): + if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: ifd[key] = info.get_ifd(key) else: ifd[key] = info.get(key) From 8d5eb71d267c7e740abe07aa9f34277d47fb5ad6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 7 Jan 2023 20:45:55 +0200 Subject: [PATCH 071/220] Use enumerate --- Tests/test_file_mpo.py | 4 +--- src/PIL/PsdImagePlugin.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 3e5476222..f0dedc2de 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -168,8 +168,7 @@ def test_mp_no_data(): def test_mp_attribute(test_file): with Image.open(test_file) as im: mpinfo = im._getmp() - frame_number = 0 - for mpentry in mpinfo[0xB002]: + for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: assert not mpattr["RepresentativeImageFlag"] @@ -180,7 +179,6 @@ def test_mp_attribute(test_file): assert mpattr["ImageDataFormat"] == "JPEG" assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" assert mpattr["Reserved"] == 0 - frame_number += 1 @pytest.mark.parametrize("test_file", test_files) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index c1ca30a03..7e8d12759 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -238,15 +238,13 @@ def _layerinfo(fp, ct_bytes): layers.append((name, mode, (x0, y0, x1, y1))) # get tiles - i = 0 - for name, mode, bbox in layers: + for i, (name, mode, bbox) in enumerate(layers): tile = [] for m in mode: t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) layers[i] = name, mode, bbox, tile - i += 1 return layers From a5e046fb4964f62837c229b9487fefe5758d1e54 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 8 Jan 2023 14:37:46 +0200 Subject: [PATCH 072/220] Convert test_properties to use parametrize --- Tests/test_image_mode.py | 44 ++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 670b2f4eb..6de2566b2 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image, ImageMode from .helper import hopper @@ -49,23 +51,25 @@ def test_sanity(): assert m.typestr == "|u1" -def test_properties(): - def check(mode, *result): - signature = ( - Image.getmodebase(mode), - Image.getmodetype(mode), - Image.getmodebands(mode), - Image.getmodebandnames(mode), - ) - assert signature == result - - check("1", "L", "L", 1, ("1",)) - check("L", "L", "L", 1, ("L",)) - check("P", "P", "L", 1, ("P",)) - check("I", "L", "I", 1, ("I",)) - check("F", "L", "F", 1, ("F",)) - check("RGB", "RGB", "L", 3, ("R", "G", "B")) - check("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")) - check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) - check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) - check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) +@pytest.mark.parametrize( + "mode, expected_base, expected_type, expected_bands, expected_band_names", + ( + ("1", "L", "L", 1, ("1",)), + ("L", "L", "L", 1, ("L",)), + ("P", "P", "L", 1, ("P",)), + ("I", "L", "I", 1, ("I",)), + ("F", "L", "F", 1, ("F",)), + ("RGB", "RGB", "L", 3, ("R", "G", "B")), + ("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")), + ("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")), + ("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")), + ("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")), + ), +) +def test_properties( + mode, expected_base, expected_type, expected_bands, expected_band_names +): + assert Image.getmodebase(mode) == expected_base + assert Image.getmodetype(mode) == expected_type + assert Image.getmodebands(mode) == expected_bands + assert Image.getmodebandnames(mode) == expected_band_names From e24dd745f7386f52d7a617a9cb5c61fbd1d0ade0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 8 Jan 2023 14:48:56 +0200 Subject: [PATCH 073/220] Convert test_optimize_correctness to use parametrize --- Tests/test_file_gif.py | 63 ++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index d48fc1442..6fbc0ee30 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -158,39 +158,42 @@ def test_optimize(): assert test_bilevel(1) == 799 -def test_optimize_correctness(): - # 256 color Palette image, posterize to > 128 and < 128 levels - # Size bigger and smaller than 512x512 +@pytest.mark.parametrize( + "colors, size, expected_palette_length", + ( + # These do optimize the palette + (256, 511, 256), + (255, 511, 255), + (129, 511, 129), + (128, 511, 128), + (64, 511, 64), + (4, 511, 4), + # These don't optimize the palette + (128, 513, 256), + (64, 513, 256), + (4, 513, 256), + ), +) +def test_optimize_correctness(colors, size, expected_palette_length): + # 256 color Palette image, posterize to > 128 and < 128 levels. + # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB - def check(colors, size, expected_palette_length): - # make an image with empty colors in the start of the palette range - im = Image.frombytes( - "P", (colors, colors), bytes(range(256 - colors, 256)) * colors - ) - im = im.resize((size, size)) - outfile = BytesIO() - im.save(outfile, "GIF") - outfile.seek(0) - with Image.open(outfile) as reloaded: - # check palette length - palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) - assert expected_palette_length == palette_length + # Check for correctness after conversion back to RGB. - assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + # make an image with empty colors in the start of the palette range + im = Image.frombytes( + "P", (colors, colors), bytes(range(256 - colors, 256)) * colors + ) + im = im.resize((size, size)) + outfile = BytesIO() + im.save(outfile, "GIF") + outfile.seek(0) + with Image.open(outfile) as reloaded: + # check palette length + palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v) + assert expected_palette_length == palette_length - # These do optimize the palette - check(256, 511, 256) - check(255, 511, 255) - check(129, 511, 129) - check(128, 511, 128) - check(64, 511, 64) - check(4, 511, 4) - - # These don't optimize the palette - check(128, 513, 256) - check(64, 513, 256) - check(4, 513, 256) + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) def test_optimize_full_l(): From 08c7b17e236d3e7a431ff40f862ca4de2a5c67df Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 9 Jan 2023 19:04:55 +0200 Subject: [PATCH 074/220] Raise ValueError for BoxBlur filter with negative radius --- Tests/test_image_filter.py | 6 ++++++ src/PIL/ImageFilter.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index cfe46b658..ece98f73d 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -24,6 +24,7 @@ from .helper import assert_image_equal, hopper ImageFilter.ModeFilter, ImageFilter.GaussianBlur, ImageFilter.GaussianBlur(5), + ImageFilter.BoxBlur(0), ImageFilter.BoxBlur(5), ImageFilter.UnsharpMask, ImageFilter.UnsharpMask(10), @@ -173,3 +174,8 @@ def test_consistency_5x5(mode): Image.merge(mode, source[: len(mode)]).filter(kernel), Image.merge(mode, reference[: len(mode)]), ) + + +def test_invalid_box_blur_filter(): + with pytest.raises(ValueError): + ImageFilter.BoxBlur(-2) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 59e2c18b9..63d6dcf5c 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -183,6 +183,9 @@ class BoxBlur(MultibandFilter): name = "BoxBlur" def __init__(self, radius): + if radius < 0: + msg = "radius must be >= 0" + raise ValueError(msg) self.radius = radius def filter(self, image): From 07a3aef3ef93cd35d102e1176eda97df9b3eb5a6 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 9 Jan 2023 20:46:07 +0100 Subject: [PATCH 075/220] list `--{dis,en}able-raqm` options in installation documentation --- docs/installation.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 42fe8c254..2a83ed151 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -369,21 +369,21 @@ Build Options available, as many as are present. * Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, - ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, - ``--disable-imagequant``, ``--disable-xcb``. + ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``, + ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, + ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, - ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, - ``--enable-imagequant``, ``--enable-xcb``. + ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``, + ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, + ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Build flags: ``--vendor-raqm --vendor-fribidi`` +* Build flags: ``--vendor-raqm``, ``--vendor-fribidi``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires From 7f57c93b89804fd6468a264cac0350403a2a097b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Jan 2023 08:50:20 +1100 Subject: [PATCH 076/220] Only read when necessary --- src/PIL/EpsImagePlugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 016e3c135..f7d376364 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -316,21 +316,22 @@ class EpsImageFile(ImageFile.ImageFile): def _find_offset(self, fp): - s = fp.read(160) + s = fp.read(4) - if s[:4] == b"%!PS": + if s == b"%!PS": # for HEAD without binary preview fp.seek(0, io.SEEK_END) length = fp.tell() offset = 0 - elif i32(s, 0) == 0xC6D3D0C5: + elif i32(s) == 0xC6D3D0C5: # FIX for: Some EPS file not handled correctly / issue #302 # EPS can contain binary data # or start directly with latin coding # more info see: # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - offset = i32(s, 4) - length = i32(s, 8) + s = fp.read(8) + offset = i32(s) + length = i32(s, 4) else: msg = "not an EPS file" raise SyntaxError(msg) From 173b65d0956e0e5f15c52b0bb46c6694446eace5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Jan 2023 20:02:10 +1100 Subject: [PATCH 077/220] Raise ValueError during filter operation as well --- Tests/test_image_filter.py | 6 ++++++ src/libImaging/BoxBlur.c | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index ece98f73d..a2ef2280b 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -179,3 +179,9 @@ def test_consistency_5x5(mode): def test_invalid_box_blur_filter(): with pytest.raises(ValueError): ImageFilter.BoxBlur(-2) + + im = hopper() + box_blur_filter = ImageFilter.BoxBlur(2) + box_blur_filter.radius = -2 + with pytest.raises(ValueError): + im.filter(box_blur_filter) diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 2e45a3358..5afe7cf50 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -237,6 +237,9 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { if (n < 1) { return ImagingError_ValueError("number of passes must be greater than zero"); } + if (radius < 0) { + return ImagingError_ValueError("radius must be >= 0"); + } if (strcmp(imIn->mode, imOut->mode) || imIn->type != imOut->type || imIn->bands != imOut->bands || imIn->xsize != imOut->xsize || From 5a2369fc33818aa85131862ad881cc1135252cfd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 11 Jan 2023 17:18:02 +0200 Subject: [PATCH 078/220] Verify the Mastodon docs link --- docs/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 674b31bd7..a4663bac8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -85,6 +85,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more + Overview ======== From 335cde81b4f813c25bd830fa7cfe2663502bb616 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jan 2023 08:41:14 +1100 Subject: [PATCH 079/220] Updated xz to 5.4.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df39260e0..a34e8b342 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.0.tar.gz/download", - "filename": "xz-5.4.0.tar.gz", - "dir": "xz-5.4.0", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.1.tar.gz/download", + "filename": "xz-5.4.1.tar.gz", + "dir": "xz-5.4.1", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From 9e4aa4e1cb817ab4b63efdbfbe7426dee2741e67 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jan 2023 09:21:25 +1100 Subject: [PATCH 080/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9a267bc9c..bf3017ca9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise ValueError for BoxBlur filter with negative radius #6874 + [hugovk, radarhere] + - Support arbitrary number of loaded modules on Windows #6761 [javidcf, radarhere, nulano] From a75a1a95142ddfac63d1a07506362083fd8faa71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jan 2023 11:49:08 +1100 Subject: [PATCH 081/220] Updated raqm to 0.10.0 --- depends/install_raqm.sh | 2 +- src/thirdparty/raqm/README.md | 4 +- src/thirdparty/raqm/raqm-version.h | 4 +- src/thirdparty/raqm/raqm.c | 554 ++++++++++++++++++++++++----- src/thirdparty/raqm/raqm.h | 15 + 5 files changed, 476 insertions(+), 103 deletions(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 992503650..d1b31cfa5 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.9.0 +archive=libraqm-0.10.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md index 3354a4d25..315e0c8d8 100644 --- a/src/thirdparty/raqm/README.md +++ b/src/thirdparty/raqm/README.md @@ -11,7 +11,7 @@ It currently provides bidirectional text support (using [FriBiDi][1] or As a result, Raqm can support most writing systems covered by Unicode. The documentation can be accessed on the web at: -> http://host-oman.github.io/libraqm/ +> https://host-oman.github.io/libraqm/ Raqm (Arabic: رَقْم) is writing, also number or digit and the Arabic word for digital (رَقَمÙيّ) shares the same root, so it is a play on “digital writingâ€. @@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm: [1]: https://github.com/fribidi/fribidi [2]: https://github.com/Tehreer/SheenBidi [3]: https://github.com/harfbuzz/harfbuzz -[4]: https://freetype.org/ +[4]: https://www.freetype.org [5]: https://www.gtk.org/gtk-doc diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index 78b70a561..bdb6fb662 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -32,10 +32,10 @@ #define _RAQM_VERSION_H_ #define RAQM_VERSION_MAJOR 0 -#define RAQM_VERSION_MINOR 9 +#define RAQM_VERSION_MINOR 10 #define RAQM_VERSION_MICRO 0 -#define RAQM_VERSION_STRING "0.9.0" +#define RAQM_VERSION_STRING "0.10.0" #define RAQM_VERSION_ATLEAST(major,minor,micro) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 13f6e1f02..770ea3018 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -171,19 +171,23 @@ typedef FriBidiLevel _raqm_bidi_level_t; #endif -typedef struct { +typedef struct +{ FT_Face ftface; int ftloadflags; hb_language_t lang; hb_script_t script; + int spacing_after; } _raqm_text_info; typedef struct _raqm_run raqm_run_t; -struct _raqm { +struct _raqm +{ int ref_count; uint32_t *text; + uint16_t *text_utf16; char *text_utf8; size_t text_len; size_t text_capacity_bytes; @@ -205,7 +209,8 @@ struct _raqm { int invisible_glyph; }; -struct _raqm_run { +struct _raqm_run +{ uint32_t pos; uint32_t len; @@ -217,9 +222,13 @@ struct _raqm_run { raqm_run_t *next; }; -static uint32_t -_raqm_u8_to_u32_index (raqm_t *rq, - uint32_t index); +static size_t +_raqm_encoding_to_u32_index (raqm_t *rq, + size_t index); + +static bool +_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, + hb_codepoint_t r_char); static void _raqm_init_text_info (raqm_t *rq) @@ -231,6 +240,7 @@ _raqm_init_text_info (raqm_t *rq) rq->text_info[i].ftloadflags = -1; rq->text_info[i].lang = default_lang; rq->text_info[i].script = HB_SCRIPT_INVALID; + rq->text_info[i].spacing_after = 0; } } @@ -263,6 +273,8 @@ _raqm_compare_text_info (_raqm_text_info a, if (a.script != b.script) return false; + /* Spacing shouldn't break runs, so we don't compare them here. */ + return true; } @@ -273,6 +285,7 @@ _raqm_free_text(raqm_t* rq) rq->text = NULL; rq->text_info = NULL; rq->text_utf8 = NULL; + rq->text_utf16 = NULL; rq->text_len = 0; rq->text_capacity_bytes = 0; } @@ -280,12 +293,15 @@ _raqm_free_text(raqm_t* rq) static bool _raqm_alloc_text(raqm_t *rq, size_t len, - bool need_utf8) + bool need_utf8, + bool need_utf16) { /* Allocate contiguous memory block for texts and text_info */ size_t mem_size = (sizeof (uint32_t) + sizeof (_raqm_text_info)) * len; if (need_utf8) mem_size += sizeof (char) * len; + else if (need_utf16) + mem_size += sizeof (uint16_t) * len; if (mem_size > rq->text_capacity_bytes) { @@ -302,6 +318,7 @@ _raqm_alloc_text(raqm_t *rq, rq->text_info = (_raqm_text_info*)(rq->text + len); rq->text_utf8 = need_utf8 ? (char*)(rq->text_info + len) : NULL; + rq->text_utf16 = need_utf16 ? (uint16_t*)(rq->text_info + len) : NULL; return true; } @@ -357,7 +374,7 @@ _raqm_free_runs (raqm_run_t *runs) * Return value: * A newly allocated #raqm_t with a reference count of 1. The initial reference * count should be released with raqm_destroy() when you are done using the - * #raqm_t. Returns %NULL in case of error. + * #raqm_t. Returns `NULL` in case of error. * * Since: 0.1 */ @@ -381,6 +398,7 @@ raqm_create (void) rq->invisible_glyph = 0; rq->text = NULL; + rq->text_utf16 = NULL; rq->text_utf8 = NULL; rq->text_info = NULL; rq->text_capacity_bytes = 0; @@ -498,7 +516,7 @@ raqm_clear_contents (raqm_t *rq) * separately can give improper output. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -518,7 +536,7 @@ raqm_set_text (raqm_t *rq, if (!len) return true; - if (!_raqm_alloc_text(rq, len, false)) + if (!_raqm_alloc_text(rq, len, false, false)) return false; rq->text_len = len; @@ -575,6 +593,53 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) return (out_utf32 - unicode); } +static void * +_raqm_get_utf16_codepoint (const void *str, + uint32_t *out_codepoint) +{ + const uint16_t *s = (const uint16_t *)str; + + if (s[0] > 0xD800 && s[0] < 0xDBFF) + { + if (s[1] > 0xDC00 && s[1] < 0xDFFF) + { + uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1)); + uint32_t W = (s[0] >> 6) & ((1 << 5) - 1); + *out_codepoint = (W+1) << 16 | X; + s += 2; + } + else + { + /* A single high surrogate, this is an error. */ + *out_codepoint = s[0]; + s += 1; + } + } + else + { + *out_codepoint = s[0]; + s += 1; + } + return (void *)s; +} + +static size_t +_raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode) +{ + size_t in_len = 0; + uint32_t *out_utf32 = unicode; + const uint16_t *in_utf16 = text; + + while ((*in_utf16 != '\0') && (in_len < len)) + { + in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); + ++out_utf32; + ++in_len; + } + + return (out_utf32 - unicode); +} + /** * raqm_set_text_utf8: * @rq: a #raqm_t. @@ -584,7 +649,7 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) * Same as raqm_set_text(), but for text encoded in UTF-8 encoding. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -604,7 +669,7 @@ raqm_set_text_utf8 (raqm_t *rq, if (!len) return true; - if (!_raqm_alloc_text(rq, len, true)) + if (!_raqm_alloc_text(rq, len, true, false)) return false; rq->text_len = _raqm_u8_to_u32 (text, len, rq->text); @@ -614,6 +679,44 @@ raqm_set_text_utf8 (raqm_t *rq, return true; } +/** + * raqm_set_text_utf16: + * @rq: a #raqm_t. + * @text: a UTF-16 encoded text string. + * @len: the length of @text in UTF-16 shorts. + * + * Same as raqm_set_text(), but for text encoded in UTF-16 encoding. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_text_utf16 (raqm_t *rq, + const uint16_t *text, + size_t len) +{ + if (!rq || !text) + return false; + + /* Call raqm_clear_contents to reuse this raqm_t */ + if (rq->text_len) + return false; + + /* Empty string, don’t fail but do nothing */ + if (!len) + return true; + + if (!_raqm_alloc_text(rq, len, false, true)) + return false; + + rq->text_len = _raqm_u16_to_u32 (text, len, rq->text); + memcpy (rq->text_utf16, text, sizeof (uint16_t) * len); + _raqm_init_text_info (rq); + + return true; +} /** * raqm_set_par_direction: * @rq: a #raqm_t. @@ -640,7 +743,7 @@ raqm_set_text_utf8 (raqm_t *rq, * text. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -673,7 +776,7 @@ raqm_set_par_direction (raqm_t *rq, * parts of the text. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Stability: * Unstable @@ -687,7 +790,7 @@ raqm_set_language (raqm_t *rq, size_t len) { hb_language_t language; - size_t end = start + len; + size_t end; if (!rq) return false; @@ -695,11 +798,8 @@ raqm_set_language (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); if (start >= rq->text_len || end > rq->text_len) return false; @@ -716,11 +816,37 @@ raqm_set_language (raqm_t *rq, return true; } +static bool +_raqm_add_font_feature (raqm_t *rq, + hb_feature_t fea) +{ + void* new_features; + + if (!rq) + return false; + + new_features = realloc (rq->features, + sizeof (hb_feature_t) * (rq->features_len + 1)); + if (!new_features) + return false; + + if (fea.start != HB_FEATURE_GLOBAL_START) + fea.start = _raqm_encoding_to_u32_index (rq, fea.start); + if (fea.end != HB_FEATURE_GLOBAL_END) + fea.end = _raqm_encoding_to_u32_index (rq, fea.end); + + rq->features = new_features; + rq->features[rq->features_len] = fea; + rq->features_len++; + + return true; +} + /** * raqm_add_font_feature: * @rq: a #raqm_t. * @feature: (transfer none): a font feature string. - * @len: length of @feature, -1 for %NULL-terminated. + * @len: length of @feature, -1 for `NULL`-terminated. * * Adds a font feature to be used by the #raqm_t during text layout. This is * usually used to turn on optional font features that are not enabled by @@ -734,7 +860,7 @@ raqm_set_language (raqm_t *rq, * end of the features list and can potentially override previous features. * * Return value: - * %true if parsing @feature succeeded, %false otherwise. + * `true` if parsing @feature succeeded, `false` otherwise. * * Since: 0.1 */ @@ -751,16 +877,7 @@ raqm_add_font_feature (raqm_t *rq, ok = hb_feature_from_string (feature, len, &fea); if (ok) - { - void* new_features = realloc (rq->features, - sizeof (hb_feature_t) * (rq->features_len + 1)); - if (!new_features) - return false; - - rq->features = new_features; - rq->features[rq->features_len] = fea; - rq->features_len++; - } + _raqm_add_font_feature (rq, fea); return ok; } @@ -817,7 +934,7 @@ _raqm_set_freetype_face (raqm_t *rq, * See also raqm_set_freetype_face_range(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -832,21 +949,23 @@ raqm_set_freetype_face (raqm_t *rq, * raqm_set_freetype_face_range: * @rq: a #raqm_t. * @face: an #FT_Face. - * @start: index of first character that should use @face. - * @len: number of characters using @face. + * @start: index of first character that should use @face from the input string. + * @len: number of elements using @face. * * Sets an #FT_Face to be used for @len-number of characters staring at @start. - * The @start and @len are input string array indices (i.e. counting bytes in - * UTF-8 and scaler values in UTF-32). + * The @start and @len are input string array indices, counting elements + * according to the underlying encoding. @start must always be aligned to the + * start of an encoded codepoint, and @len must always end at a codepoint's + * final element. * * This method can be used repeatedly to set different faces for different * parts of the text. It is the responsibility of the client to make sure that - * face ranges cover the whole text. + * face ranges cover the whole text, and is properly aligned. * * See also raqm_set_freetype_face(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.1 */ @@ -856,7 +975,7 @@ raqm_set_freetype_face_range (raqm_t *rq, size_t start, size_t len) { - size_t end = start + len; + size_t end; if (!rq) return false; @@ -864,11 +983,8 @@ raqm_set_freetype_face_range (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); return _raqm_set_freetype_face (rq, face, start, end); } @@ -909,7 +1025,7 @@ _raqm_set_freetype_load_flags (raqm_t *rq, * older version the flags will be ignored. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.3 */ @@ -943,7 +1059,7 @@ raqm_set_freetype_load_flags (raqm_t *rq, * See also raqm_set_freetype_load_flags(). * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.9 */ @@ -953,7 +1069,7 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, size_t start, size_t len) { - size_t end = start + len; + size_t end; if (!rq) return false; @@ -961,15 +1077,161 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) - { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); - } + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); return _raqm_set_freetype_load_flags (rq, flags, start, end); } +static bool +_raqm_set_spacing (raqm_t *rq, + int spacing, + bool word_spacing, + size_t start, + size_t end) +{ + if (!rq) + return false; + + if (!rq->text_len) + return true; + + if (start >= rq->text_len || end > rq->text_len) + return false; + + if (!rq->text_info) + return false; + + for (size_t i = start; i < end; i++) + { + bool set_spacing = i == 0; + if (!set_spacing) + set_spacing = _raqm_allowed_grapheme_boundary (rq->text[i-1], rq->text[i]); + + if (set_spacing) + { + if (word_spacing) + { + if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1])) + { + /* CSS word seperators, word spacing is only applied on these.*/ + if (rq->text[i] == 0x0020 || /* Space */ + rq->text[i] == 0x00A0 || /* No Break Space */ + rq->text[i] == 0x1361 || /* Ethiopic Word Space */ + rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ + rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ + rq->text[i] == 0x1039F || /* Ugaric Word Divider */ + rq->text[i] == 0x1091F) /* Phoenician Word Separator */ + { + rq->text_info[i].spacing_after = spacing; + } + } + } + else + { + rq->text_info[i].spacing_after = spacing; + } + } + } + + return true; +} + +/** + * raqm_set_letter_spacing_range: + * @rq: a #raqm_t. + * @spacing: amount of spacing in Freetype Font Units (26.6 format). + * @start: index of first character that should use @spacing. + * @len: number of characters using @spacing. + * + * Set the letter spacing or tracking for a given range, the value + * will be added onto the advance and offset for RTL, and the advance for + * other directions. Letter spacing will be applied between characters, so + * the last character will not have spacing applied after it. + * Note that not all scripts have a letter-spacing tradition, + * for example, Arabic does not, while Devanagari does. + * + * This will also add “disable `liga`, `clig`, `hlig`, `dlig`, and `calt`†font + * features to the internal features list, so call this function after setting + * the font features for best spacing results. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_letter_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = start + len - 1; + + if (spacing != 0) + { +#define NUM_TAGS 5 + static char *tags[NUM_TAGS] = { "clig", "liga", "hlig", "dlig", "calt" }; + for (size_t i = 0; i < NUM_TAGS; i++) + { + hb_feature_t fea = { hb_tag_from_string(tags[i], 5), 0, start, end }; + _raqm_add_font_feature (rq, fea); + } +#undef NUM_TAGS + } + + start = _raqm_encoding_to_u32_index (rq, start); + end = _raqm_encoding_to_u32_index (rq, end); + + return _raqm_set_spacing (rq, spacing, false, start, end); +} + +/** + * raqm_set_word_spacing_range: + * @rq: a #raqm_t. + * @spacing: amount of spacing in Freetype Font Units (26.6 format). + * @start: index of first character that should use @spacing. + * @len: number of characters using @spacing. + * + * Set the word spacing for a given range. Word spacing will only be applied to + * 'word separator' characters, such as 'space', 'no break space' and + * Ethiopic word separator'. + * The value will be added onto the advance and offset for RTL, and the advance + * for other directions. + * + * Return value: + * `true` if no errors happened, `false` otherwise. + * + * Since: 0.10 + */ +bool +raqm_set_word_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len) +{ + size_t end; + + if (!rq) + return false; + + if (!rq->text_len) + return true; + + end = _raqm_encoding_to_u32_index (rq, start + len); + start = _raqm_encoding_to_u32_index (rq, start); + + return _raqm_set_spacing (rq, spacing, true, start, end); +} + /** * raqm_set_invisible_glyph: * @rq: a #raqm_t. @@ -984,7 +1246,7 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, * If @gid is a positive number, it will be used for invisible glyphs. * * Return value: - * %true if no errors happened, %false otherwise. + * `true` if no errors happened, `false` otherwise. * * Since: 0.6 */ @@ -1014,7 +1276,7 @@ _raqm_shape (raqm_t *rq); * text shaping, and any other part of the layout process. * * Return value: - * %true if the layout process was successful, %false otherwise. + * `true` if the layout process was successful, `false` otherwise. * * Since: 0.1 */ @@ -1048,7 +1310,9 @@ raqm_layout (raqm_t *rq) static uint32_t _raqm_u32_to_u8_index (raqm_t *rq, uint32_t index); - +static uint32_t +_raqm_u32_to_u16_index (raqm_t *rq, + uint32_t index); /** * raqm_get_glyphs: * @rq: a #raqm_t. @@ -1059,7 +1323,7 @@ _raqm_u32_to_u8_index (raqm_t *rq, * information. * * Return value: (transfer none): - * An array of #raqm_glyph_t, or %NULL in case of error. This is owned by @rq + * An array of #raqm_glyph_t, or `NULL` in case of error. This is owned by @rq * and must not be freed. * * Since: 0.1 @@ -1147,6 +1411,12 @@ raqm_get_glyphs (raqm_t *rq, RAQM_TEST ("\n"); #endif } + else if (rq->text_utf16) + { + for (size_t i = 0; i < count; i++) + rq->glyphs[i].cluster = _raqm_u32_to_u16_index (rq, + rq->glyphs[i].cluster); + } return rq->glyphs; } @@ -1194,8 +1464,10 @@ raqm_get_direction_at_index (raqm_t *rq, for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) { - if (run->pos <= index && index < run->pos + run->len) { - switch (run->direction) { + if (run->pos <= index && index < run->pos + run->len) + { + switch (run->direction) + { case HB_DIRECTION_LTR: return RAQM_DIRECTION_LTR; case HB_DIRECTION_RTL: @@ -1227,7 +1499,8 @@ _raqm_hb_dir (raqm_t *rq, _raqm_bidi_level_t level) return dir; } -typedef struct { +typedef struct +{ size_t pos; size_t len; _raqm_bidi_level_t level; @@ -1264,10 +1537,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count) line = SBParagraphCreateLine (par, 0, par_len); *run_count = SBLineGetRunCount (line); - if (SBParagraphGetBaseLevel (par) == 0) - rq->resolved_dir = RAQM_DIRECTION_LTR; - else + if (SBParagraphGetBaseLevel (par) == 1) rq->resolved_dir = RAQM_DIRECTION_RTL; + else + rq->resolved_dir = RAQM_DIRECTION_LTR; runs = malloc (sizeof (_raqm_bidi_run) * (*run_count)); if (runs) @@ -1418,10 +1691,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count) rq->text_len, &par_type, levels); - if (par_type == FRIBIDI_PAR_LTR) - rq->resolved_dir = RAQM_DIRECTION_LTR; - else + if (par_type == FRIBIDI_PAR_RTL) rq->resolved_dir = RAQM_DIRECTION_RTL; + else + rq->resolved_dir = RAQM_DIRECTION_LTR; if (max_level == 0) goto done; @@ -1447,22 +1720,15 @@ _raqm_itemize (raqm_t *rq) bool ok = true; #ifdef RAQM_TESTING - switch (rq->base_dir) - { - case RAQM_DIRECTION_RTL: - RAQM_TEST ("Direction is: RTL\n\n"); - break; - case RAQM_DIRECTION_LTR: - RAQM_TEST ("Direction is: LTR\n\n"); - break; - case RAQM_DIRECTION_TTB: - RAQM_TEST ("Direction is: TTB\n\n"); - break; - case RAQM_DIRECTION_DEFAULT: - default: - RAQM_TEST ("Direction is: DEFAULT\n\n"); - break; - } + static char *dir_names[] = { + "DEFAULT", + "RTL", + "LTR", + "TTB" + }; + + assert (rq->base_dir < sizeof (dir_names)); + RAQM_TEST ("Direction is: %s\n\n", dir_names[rq->base_dir]); #endif if (!_raqm_resolve_scripts (rq)) @@ -1483,9 +1749,9 @@ _raqm_itemize (raqm_t *rq) runs->len = rq->text_len; runs->level = 0; } - } else { - runs = _raqm_bidi_itemize (rq, &run_count); } + else + runs = _raqm_bidi_itemize (rq, &run_count); if (!runs) { @@ -1494,6 +1760,9 @@ _raqm_itemize (raqm_t *rq) } #ifdef RAQM_TESTING + assert (rq->resolved_dir < sizeof (dir_names)); + if (rq->base_dir == RAQM_DIRECTION_DEFAULT) + RAQM_TEST ("Resolved direction is: %s\n\n", dir_names[rq->resolved_dir]); RAQM_TEST ("Number of runs before script itemization: %zu\n\n", run_count); RAQM_TEST ("BiDi Runs:\n"); @@ -1617,7 +1886,8 @@ done: } /* Stack to handle script detection */ -typedef struct { +typedef struct +{ size_t capacity; size_t size; int *pair_index; @@ -1910,15 +2180,47 @@ _raqm_shape (raqm_t *rq) { FT_Matrix matrix; + hb_glyph_info_t *info; hb_glyph_position_t *pos; unsigned int len; FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); pos = hb_buffer_get_glyph_positions (run->buffer, &len); + info = hb_buffer_get_glyph_infos (run->buffer, &len); + for (unsigned int i = 0; i < len; i++) { _raqm_ft_transform (&pos[i].x_advance, &pos[i].y_advance, matrix); _raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix); + + bool set_spacing = false; + if (run->direction == HB_DIRECTION_RTL) + { + set_spacing = i == 0; + if (!set_spacing) + set_spacing = info[i].cluster != info[i-1].cluster; + } + else + { + set_spacing = i == len - 1; + if (!set_spacing) + set_spacing = info[i].cluster != info[i+1].cluster; + } + + _raqm_text_info rq_info = rq->text_info[info[i].cluster]; + + if (rq_info.spacing_after != 0 && set_spacing) + { + if (run->direction == HB_DIRECTION_TTB) + pos[i].y_advance -= rq_info.spacing_after; + else if (run->direction == HB_DIRECTION_RTL) + { + pos[i].x_advance += rq_info.spacing_after; + pos[i].x_offset += rq_info.spacing_after; + } + else + pos[i].x_advance += rq_info.spacing_after; + } } } } @@ -1954,9 +2256,9 @@ _raqm_u32_to_u8_index (raqm_t *rq, } /* Convert index from UTF-8 to UTF-32 */ -static uint32_t +static size_t _raqm_u8_to_u32_index (raqm_t *rq, - uint32_t index) + size_t index) { const unsigned char *s = (const unsigned char *) rq->text_utf8; const unsigned char *t = s; @@ -1982,9 +2284,64 @@ _raqm_u8_to_u32_index (raqm_t *rq, return length; } -static bool -_raqm_allowed_grapheme_boundary (hb_codepoint_t l_char, - hb_codepoint_t r_char); +/* Count equivalent UTF-16 short in codepoint */ +static size_t +_raqm_count_codepoint_utf16_short (uint32_t chr) +{ + if (chr > 0x010000) + return 2; + else + return 1; +} + +/* Convert index from UTF-32 to UTF-16 */ +static uint32_t +_raqm_u32_to_u16_index (raqm_t *rq, + uint32_t index) +{ + size_t length = 0; + + for (uint32_t i = 0; i < index; ++i) + length += _raqm_count_codepoint_utf16_short (rq->text[i]); + + return length; +} + +/* Convert index from UTF-16 to UTF-32 */ +static size_t +_raqm_u16_to_u32_index (raqm_t *rq, + size_t index) +{ + const uint16_t *s = (const uint16_t *) rq->text_utf16; + const uint16_t *t = s; + size_t length = 0; + + while (((size_t) (s - t) < index) && ('\0' != *s)) + { + if (*s < 0xD800 || *s > 0xDBFF) + s += 1; + else + s += 2; + + length++; + } + + if ((size_t) (s-t) > index) + length--; + + return length; +} + +static inline size_t +_raqm_encoding_to_u32_index (raqm_t *rq, + size_t index) +{ + if (rq->text_utf8) + return _raqm_u8_to_u32_index (rq, index); + else if (rq->text_utf16) + return _raqm_u16_to_u32_index (rq, index); + return index; +} static bool _raqm_in_hangul_syllable (hb_codepoint_t ch); @@ -2001,7 +2358,7 @@ _raqm_in_hangul_syllable (hb_codepoint_t ch); * character is left-to-right, then the cursor will be at the right of it. * * Return value: - * %true if the process was successful, %false otherwise. + * `true` if the process was successful, `false` otherwise. * * Since: 0.2 */ @@ -2018,8 +2375,7 @@ raqm_index_to_position (raqm_t *rq, if (rq == NULL) return false; - if (rq->text_utf8) - *index = _raqm_u8_to_u32_index (rq, *index); + *index = _raqm_encoding_to_u32_index (rq, *index); if (*index >= rq->text_len) return false; @@ -2077,6 +2433,8 @@ raqm_index_to_position (raqm_t *rq, found: if (rq->text_utf8) *index = _raqm_u32_to_u8_index (rq, *index); + else if (rq->text_utf16) + *index = _raqm_u32_to_u16_index (rq, *index); RAQM_TEST ("The position is %d at index %zu\n",*x ,*index); return true; } @@ -2093,7 +2451,7 @@ found: * @index. * * Return value: - * %true if the process was successful, %false in case of error. + * `true` if the process was successful, `false` in case of error. * * Since: 0.2 */ @@ -2371,8 +2729,8 @@ raqm_version_string (void) * Checks if library version is less than or equal the specified version. * * Return value: - * %true if library version is less than or equal the specfied version, %false - * otherwise. + * `true` if library version is less than or equal the specified version, + * `false` otherwise. * * Since: 0.7 **/ @@ -2393,8 +2751,8 @@ raqm_version_atleast (unsigned int major, * Checks if library version is less than or equal the specified version. * * Return value: - * %true if library version is less than or equal the specfied version, %false - * otherwise. + * `true` if library version is less than or equal the specified version, + * `false` otherwise. * * Since: 0.7 **/ diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h index bdb5a50d8..2fd836c86 100644 --- a/src/thirdparty/raqm/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -118,6 +118,10 @@ RAQM_API bool raqm_set_text_utf8 (raqm_t *rq, const char *text, size_t len); +RAQM_API bool +raqm_set_text_utf16 (raqm_t *rq, + const uint16_t *text, + size_t len); RAQM_API bool raqm_set_par_direction (raqm_t *rq, @@ -154,6 +158,17 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, size_t start, size_t len); +RAQM_API bool +raqm_set_letter_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len); +RAQM_API bool +raqm_set_word_spacing_range(raqm_t *rq, + int spacing, + size_t start, + size_t len); + RAQM_API bool raqm_set_invisible_glyph (raqm_t *rq, int gid); From ad46630cdfa3cd9d0c2fe1de6e599f6aa49355c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Jan 2023 18:04:41 +1100 Subject: [PATCH 082/220] Updated macOS tested Pillow versions [ci skip] --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 2a83ed151..cc7d0258b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -478,13 +478,13 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | +---------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 | +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | | +---------------------------+------------------+ | | | 3.6 | 8.4.0 | | +----------------------------------+---------------------------+------------------+--------------+ From a2edefb45532d20cda0a2915d3f7363dd6ad8754 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jan 2023 07:18:56 +1100 Subject: [PATCH 083/220] Only install python-pyqt6 package on 64-bit --- .github/workflows/test-mingw.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ccf6e193a..6a60bc7f0 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -49,7 +49,6 @@ jobs: ${{ matrix.package }}-python3-numpy \ ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python-pyqt6 \ ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ @@ -63,6 +62,11 @@ jobs: ${{ matrix.package }}-openjpeg2 \ subversion + if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then + pacman -S --noconfirm \ + ${{ matrix.package }}-python-pyqt6 + fi + python3 -m pip install pyroma pytest pytest-cov pytest-timeout pushd depends && ./install_extra_test_images.sh && popd From ba0d71fec84aff535b0e497a6e2e65b87ad1261b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jan 2023 15:59:51 +1100 Subject: [PATCH 084/220] Updated libwebp to 1.3.0 --- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 05867b7d4..f8b985a7a 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.2.4 +archive=libwebp-1.3.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a34e8b342..fd12240e5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -177,9 +177,9 @@ deps = { "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", - "filename": "libwebp-1.2.4.tar.gz", - "dir": "libwebp-1.2.4", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz", + "filename": "libwebp-1.3.0.tar.gz", + "dir": "libwebp-1.3.0", "license": "COPYING", "build": [ cmd_rmdir(r"output\release-static"), # clean From e48aead015996de2284ebbbbc4a00a726d61af9b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jan 2023 21:02:42 +1100 Subject: [PATCH 085/220] Allow writing IFDRational to BYTE tag --- Tests/test_file_tiff_metadata.py | 5 +++-- src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 48797ea08..1061f7d05 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -202,14 +202,15 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path): assert reloaded.tag_v2[271] == expected -def test_writing_int_to_bytes(tmp_path): +@pytest.mark.parametrize("value", (1, IFDRational(1))) +def test_writing_other_types_to_bytes(value, tmp_path): im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[700] assert tag.type == TiffTags.BYTE - info[700] = 1 + info[700] = value out = str(tmp_path / "temp.tiff") im.save(out, tiffinfo=info) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 431a95701..baa9abad8 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -722,6 +722,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(1) # Basic type, except for the legacy API. def write_byte(self, data): + if isinstance(data, IFDRational): + data = int(data) if isinstance(data, int): data = bytes((data,)) return data From 43bb03539e0f3dca6ee399cbb8162c21e257c05d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jan 2023 20:02:16 +1100 Subject: [PATCH 086/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bf3017ca9..b3dd16ace 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow writing IFDRational to BYTE tag #6890 + [radarhere] + - Raise ValueError for BoxBlur filter with negative radius #6874 [hugovk, radarhere] From c5d1b1582452d7314cbd8f2b1cd9fe58d1fc6894 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jan 2023 22:45:29 +1100 Subject: [PATCH 087/220] Do not unintentionally load TIFF format at first --- src/PIL/JpegImagePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 9657ae9d0..b9c80236e 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -41,7 +41,7 @@ import sys import tempfile import warnings -from . import Image, ImageFile, TiffImagePlugin +from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 @@ -524,6 +524,8 @@ def _getmp(self): head = file_contents.read(8) endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<" # process dictionary + from . import TiffImagePlugin + try: info = TiffImagePlugin.ImageFileDirectory_v2(head) file_contents.seek(info.next) From 5f9285eea6b46d675daf5f6d733efa872086760d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Jan 2023 23:22:35 +1100 Subject: [PATCH 088/220] Do not retry specified formats if they failed --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b0ff5173c..7fc8f496e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3267,7 +3267,7 @@ def open(fp, mode="r", formats=None): im = _open_core(fp, filename, prefix, formats) - if im is None: + if im is None and formats is ID: if init(): im = _open_core(fp, filename, prefix, formats) From 55ce251a8943f419701242ed06d366df2b5a28e3 Mon Sep 17 00:00:00 2001 From: Alex Clark Date: Sat, 14 Jan 2023 12:36:22 -0500 Subject: [PATCH 089/220] Alex Clark -> Jeffrey A. Clark (Alex) I'm still "Alex", just on a Jeffrey A. Clark roll lately. --- LICENSE | 2 +- README.md | 4 ++-- docs/COPYING | 2 +- docs/conf.py | 8 ++++---- docs/index.rst | 2 +- setup.cfg | 4 ++-- src/PIL/__init__.py | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/LICENSE b/LICENSE index 616808a48..125bdcc44 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2023 by Alex Clark and contributors + Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors. Like PIL, Pillow is licensed under the open source HPND License: diff --git a/README.md b/README.md index 489d3db54..af1ca57c2 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ ## Python Imaging Library (Fork) -Pillow is the friendly PIL fork by [Alex Clark and -Contributors](https://github.com/python-pillow/Pillow/graphs/contributors). +Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and +contributors](https://github.com/python-pillow/Pillow/graphs/contributors). PIL is the Python Imaging Library by Fredrik Lundh and Contributors. As of 2019, Pillow development is [supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). diff --git a/docs/COPYING b/docs/COPYING index b400381d3..bc44ba388 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2023 by Alex Clark and contributors + Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index fb58d25ed..96324423a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,8 +52,8 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2023 Alex Clark and Contributors" -author = "Fredrik Lundh, Alex Clark and Contributors" +copyright = "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" +author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -243,7 +243,7 @@ latex_documents = [ master_doc, "PillowPILFork.tex", "Pillow (PIL Fork) Documentation", - "Alex Clark", + "Jeffrey A. Clark (Alex)", "manual", ) ] @@ -293,7 +293,7 @@ texinfo_documents = [ "Pillow (PIL Fork) Documentation", author, "PillowPILFork", - "Pillow is the friendly PIL fork by Alex Clark and Contributors.", + "Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors.", "Miscellaneous", ) ] diff --git a/docs/index.rst b/docs/index.rst index a4663bac8..418844ba7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Pillow ====== -Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow is the friendly PIL fork by `Jeffrey A. Clark (Alex) and contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and contributors. Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. diff --git a/setup.cfg b/setup.cfg index 2dc552a2c..824cae088 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,8 +4,8 @@ description = Python Imaging Library (Fork) long_description = file: README.md long_description_content_type = text/markdown url = https://python-pillow.org -author = Alex Clark (PIL Fork Author) -author_email = aclark@python-pillow.org +author = Jeffrey A. Clark (Alex) +author_email = aclark@aclark.net license = HPND classifiers = Development Status :: 6 - Mature diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 4b76e893f..0e6f82092 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,11 +1,11 @@ """Pillow (Fork of the Python Imaging Library) -Pillow is the friendly PIL fork by Alex Clark and Contributors. +Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors. https://github.com/python-pillow/Pillow/ Pillow is forked from PIL 1.1.7. -PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +PIL is the Python Imaging Library by Fredrik Lundh and contributors. Copyright (c) 1999 by Secret Labs AB. Use PIL.__version__ for this Pillow version. From 5a71fe804154f627cd9ba28eafe47c893a0d6ea6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 17:39:33 +0000 Subject: [PATCH 090/220] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 96324423a..e1ffa49b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,9 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" +copyright = ( + "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" +) author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" # The version info for the project you're documenting, acts as replacement for From 3360b5a756f761051653d3c854a7601cea33d064 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Jan 2023 19:49:13 +1100 Subject: [PATCH 091/220] Stop reading when a line becomes too long --- src/PIL/EpsImagePlugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index f7d376364..dd68c13e5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -173,11 +173,13 @@ class PSFile: self.fp.seek(offset, whence) def readline(self): - s = [self.char or b""] - self.char = None + s = [] + if self.char: + s.append(self.char) + self.char = None c = self.fp.read(1) - while (c not in b"\r\n") and len(c): + while (c not in b"\r\n") and len(c) and len(b"".join(s).strip(b"\r\n")) <= 255: s.append(c) c = self.fp.read(1) From 04cf5e2cfc5dc1676efd9f5c8d605ddfccb0f9ee Mon Sep 17 00:00:00 2001 From: Bas Couwenberg Date: Sat, 14 Jan 2023 19:09:43 +0100 Subject: [PATCH 092/220] Handle more than one directory returned by pkg-config. tiff (4.5.0-1) in Debian results in two include directories being returned: ``` -I/usr/include/x86_64-linux-gnu -I/usr/include ``` --- setup.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 243365681..b4ebbb9c2 100755 --- a/setup.py +++ b/setup.py @@ -263,18 +263,20 @@ def _pkg_config(name): if not DEBUG: command_libs.append("--silence-errors") command_cflags.append("--silence-errors") - libs = ( + libs = re.split( + r"\s*-L", subprocess.check_output(command_libs, stderr=stderr) .decode("utf8") - .strip() - .replace("-L", "") + .strip(), ) - cflags = ( - subprocess.check_output(command_cflags) + libs.remove("") + cflags = re.split( + r"\s*-I", + subprocess.check_output(command_cflags, stderr=stderr) .decode("utf8") - .strip() - .replace("-I", "") + .strip(), ) + cflags.remove("") return libs, cflags except Exception: pass @@ -473,8 +475,12 @@ class pil_build_ext(build_ext): else: lib_root = include_root = root - _add_directory(library_dirs, lib_root) - _add_directory(include_dirs, include_root) + if lib_root is not None: + for lib_dir in lib_root: + _add_directory(library_dirs, lib_dir) + if include_root is not None: + for include_dir in include_root: + _add_directory(include_dirs, include_dir) # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): From cd4656410f8d8ddcf806717e7404bc2f0392d88d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 17:32:58 -0600 Subject: [PATCH 093/220] parametrize test_file_tar::test_sanity() --- Tests/test_file_tar.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 5daab47fc..49451ff44 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -10,18 +10,21 @@ from .helper import is_pypy TEST_TAR_FILE = "Tests/images/hopper.tar" -def test_sanity(): - for codec, test_path, format in [ - ["zlib", "hopper.png", "PNG"], - ["jpg", "hopper.jpg", "JPEG"], - ]: - if features.check(codec): - with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: - with Image.open(tar) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == format +@pytest.mark.parametrize( + ("codec", "test_path", "format"), + ( + ("zlib", "hopper.png", "PNG"), + ("jpg", "hopper.jpg", "JPEG"), + ), +) +def test_sanity(codec, test_path, format): + if features.check(codec): + with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: + with Image.open(tar) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == format @pytest.mark.skipif(is_pypy(), reason="Requires CPython") From c2176f2747cd64cd2cf1d7ba859fde1a26f3db52 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 19:36:52 -0600 Subject: [PATCH 094/220] use string for parametrization name declaration Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_tar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 49451ff44..799c243d6 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -11,7 +11,7 @@ TEST_TAR_FILE = "Tests/images/hopper.tar" @pytest.mark.parametrize( - ("codec", "test_path", "format"), + "codec, test_path, format", ( ("zlib", "hopper.png", "PNG"), ("jpg", "hopper.jpg", "JPEG"), From 7ad50d9185a7beef755ea7c19b8cfe6e5bea815e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 19:42:55 -0600 Subject: [PATCH 095/220] 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 096/220] 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 097/220] 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 2332a1c796eaee8f79cf3d1772c1c5352a4c977b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jan 2023 08:27:49 +1100 Subject: [PATCH 098/220] Updated libimagequant to 4.0.5 --- 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 64dd024bd..541ec8fda 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.4 +archive=libimagequant-4.0.5 ./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 cc7d0258b..0cea725b4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -169,7 +169,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.4** + * Pillow has been tested with libimagequant **2.6-4.0.5** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 0635c180307850db3737e25e8efa49502bf38db6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jan 2023 23:02:19 +1100 Subject: [PATCH 099/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b3dd16ace..ed41d46c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Stop reading when EPS line becomes too long #6897 + [radarhere] + - Allow writing IFDRational to BYTE tag #6890 [radarhere] From bf0abdca27cd84dafd185bd44206c82b5c14330d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Jan 2023 08:06:30 +1100 Subject: [PATCH 100/220] Do not retry past formats when loading all formats for the first time --- src/PIL/Image.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7fc8f496e..833473f78 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3268,8 +3268,14 @@ def open(fp, mode="r", formats=None): im = _open_core(fp, filename, prefix, formats) if im is None and formats is ID: + checked_formats = formats.copy() if init(): - im = _open_core(fp, filename, prefix, formats) + im = _open_core( + fp, + filename, + prefix, + tuple(format for format in formats if format not in checked_formats), + ) if im: im._exclusive_fp = exclusive_fp From 9b660db62de0cac22f2d1bf37aabbd412ee7bc62 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 20 Jan 2023 14:35:11 +0200 Subject: [PATCH 101/220] Handling for deprecations to be removed in Pillow 11 --- Tests/test_deprecate.py | 5 +++++ src/PIL/_deprecate.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 30ed4a808..3375eb6b2 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -11,6 +11,11 @@ from PIL import _deprecate "Old thing is deprecated and will be removed in Pillow 10 " r"\(2023-07-01\)\. Use new thing instead\.", ), + ( + 11, + "Old thing is deprecated and will be removed in Pillow 11 " + r"\(2024-10-15\)\. Use new thing instead\.", + ), ( None, r"Old thing is deprecated and will be removed in a future version\. " diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 7c4b1623d..fa6e1d00c 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -47,6 +47,8 @@ def deprecate( raise RuntimeError(msg) elif when == 10: removed = "Pillow 10 (2023-07-01)" + elif when == 11: + removed = "Pillow 11 (2024-10-15)" else: msg = f"Unknown removal version, update {__name__}?" raise ValueError(msg) From e01f5556586a0f789c0ae0fc00d306a2a6513c3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Jan 2023 06:41:19 +1100 Subject: [PATCH 102/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ed41d46c7..27dbd69bb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Do not retry past formats when loading all formats for the first time #6902 + [radarhere] + +- Do not retry specified formats if they failed when opening #6893 + [radarhere] + +- Do not unintentionally load TIFF format at first #6892 + [radarhere] + - Stop reading when EPS line becomes too long #6897 [radarhere] From 20c54ba1108c831484e2e22c5e143952259a67bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Jan 2023 07:37:20 +1100 Subject: [PATCH 103/220] Updated libimagequant to 4.1.0 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 541ec8fda..8b847b894 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.5 +archive=libimagequant-4.1.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index 0cea725b4..ea8722c56 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -169,7 +169,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.5** + * Pillow has been tested with libimagequant **2.6-4.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 9dc9e82bef9462ee17cdc9c623654a75eab64a6d Mon Sep 17 00:00:00 2001 From: Renat Nasyrov Date: Tue, 24 Jan 2023 00:11:27 +0100 Subject: [PATCH 104/220] Specify correct description for mode L. --- docs/handbook/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 45c662bd6..0aa2f1119 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -31,7 +31,7 @@ INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current re supports the following standard modes: * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) - * ``L`` (8-bit pixels, black and white) + * ``L`` (8-bit pixels, grayscale) * ``P`` (8-bit pixels, mapped to any other mode using a color palette) * ``RGB`` (3x8-bit pixels, true color) * ``RGBA`` (4x8-bit pixels, true color with transparency mask) From e76fa1674e65a38092dcb4b7cbafb2eaaaaaa6c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 14:15:51 +1100 Subject: [PATCH 105/220] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 1fc816146..34609314c 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -6,7 +6,7 @@ with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecationWarning) from PIL import ImageQt -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import assert_image_equal_tofile, assert_image_similar, hopper if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_equal(result, expected.convert("RGB")) + assert_image_similar(result, expected.convert("RGB"), 0.3) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 73f55b4e01162c075f9955a5b3eaf86182ddebce Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 1 Jan 2023 19:32:21 +0100 Subject: [PATCH 106/220] remove redundant default value Co-authored-by: Andrew Murray --- Tests/helper.py | 4 ++-- setup.py | 4 +--- src/PIL/ImageFont.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 0d1d03ac8..69246bfcf 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) HAS_UPLOADER = False -if os.environ.get("SHOW_ERRORS", None): +if os.environ.get("SHOW_ERRORS"): # local img.show for errors. HAS_UPLOADER = True @@ -271,7 +271,7 @@ def netpbm_available(): def magick_command(): if sys.platform == "win32": - magickhome = os.environ.get("MAGICK_HOME", "") + magickhome = os.environ.get("MAGICK_HOME") if magickhome: imagemagick = [os.path.join(magickhome, "convert.exe")] graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] diff --git a/setup.py b/setup.py index 243365681..e7f4f476c 100755 --- a/setup.py +++ b/setup.py @@ -567,9 +567,7 @@ class pil_build_ext(build_ext): ): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) - if sys.platform.startswith("linux") and os.environ.get( - "ANDROID_ROOT", None - ): + if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): # termux support for android. # system libraries (zlib) are installed in /system/lib # headers are at $PREFIX/include diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b144c3dd2..81e9a640f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -1020,7 +1020,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): if windir: dirs.append(os.path.join(windir, "fonts")) elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS", "") + lindirs = os.environ.get("XDG_DATA_DIRS") if not lindirs: # According to the freedesktop spec, XDG_DATA_DIRS should # default to /usr/share From a0492f796876c2a9b8ba445d72c771b84eff93a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 19:27:51 +1100 Subject: [PATCH 107/220] Ensure that pkg-config paths are split by spaces --- setup.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b4ebbb9c2..4382c1a97 100755 --- a/setup.py +++ b/setup.py @@ -264,19 +264,17 @@ def _pkg_config(name): command_libs.append("--silence-errors") command_cflags.append("--silence-errors") libs = re.split( - r"\s*-L", + r"(^|\s+)-L", subprocess.check_output(command_libs, stderr=stderr) .decode("utf8") .strip(), - ) - libs.remove("") + )[::2][1:] cflags = re.split( - r"\s*-I", + r"(^|\s+)-I", subprocess.check_output(command_cflags, stderr=stderr) .decode("utf8") .strip(), - ) - cflags.remove("") + )[::2][1:] return libs, cflags except Exception: pass From 3e37a919b136d35447bde7694ecf579a2096b163 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 22:43:04 +1100 Subject: [PATCH 108/220] Prevent register_open from adding duplicates to ID --- Tests/test_image.py | 11 +++++++++++ src/PIL/Image.py | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index d261638f9..ad3346b5a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -398,6 +398,17 @@ class TestImage: with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), (0, -1)) + def test_register_open_duplicates(self): + # Arrange + factory, accept = Image.OPEN["JPEG"] + id_length = len(Image.ID) + + # Act + Image.register_open("JPEG", factory, accept) + + # Assert + assert len(Image.ID) == id_length + def test_registered_extensions_uninitialized(self): # Arrange Image._initialized = 0 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 833473f78..ad0d25add 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3406,7 +3406,8 @@ def register_open(id, factory, accept=None): reject images having another format. """ id = id.upper() - ID.append(id) + if id not in ID: + ID.append(id) OPEN[id] = factory, accept From 510de501ead4fab2d85e84f2f48ec98438dc0c9a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 17:18:17 +1100 Subject: [PATCH 109/220] 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 110/220] Moved test_wrong_bits_per_sample to test_file_libtiff.py --- Tests/test_file_libtiff.py | 30 ++++++++++++++++++++++++++++++ Tests/test_file_tiff.py | 30 ------------------------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 6e111ebfc..26feeb540 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -986,6 +986,36 @@ class TestFileLibTiff(LibTiffTestCase): ) as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + @pytest.mark.parametrize( + "file_name,mode,size,tile", + [ + ( + "tiff_wrong_bits_per_sample.tiff", + "RGBA", + (52, 53), + [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_2.tiff", + "RGB", + (16, 16), + [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_3.tiff", + "RGBA", + (512, 256), + [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], + ), + ], + ) + def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + with Image.open("Tests/images/" + file_name) as im: + assert im.mode == mode + assert im.size == size + assert im.tile == tile + im.load() + def test_no_rows_per_strip(self): # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 96db4cb5e..cd8a00ab0 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -100,36 +100,6 @@ class TestFileTiff: with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") - @pytest.mark.parametrize( - "file_name,mode,size,tile", - [ - ( - "tiff_wrong_bits_per_sample.tiff", - "RGBA", - (52, 53), - [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_2.tiff", - "RGB", - (16, 16), - [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_3.tiff", - "RGBA", - (512, 256), - [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], - ), - ], - ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile): - with Image.open("Tests/images/" + file_name) as im: - assert im.mode == mode - assert im.size == size - assert im.tile == tile - im.load() - def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: From 1de6c958dfedbe9762266d73559dfc1635b7744c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 18:43:40 +1100 Subject: [PATCH 111/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 27dbd69bb..e35a55965 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Handle more than one directory returned by pkg-config #6896 + [sebastic, radarhere] + - Do not retry past formats when loading all formats for the first time #6902 [radarhere] From 446cfddb5d11ae678ccb7a8aac4c948b9555fd3d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 20:05:35 +1100 Subject: [PATCH 112/220] pre-commit autoupdate --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d790e7850..5214d352d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort @@ -26,7 +26,7 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.1 + rev: v1.4.2 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) @@ -39,7 +39,7 @@ repos: [flake8-2020, flake8-errmsg, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-check-blanket-noqa - id: rst-backticks @@ -57,7 +57,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.5.2 + rev: 0.6.1 hooks: - id: tox-ini-fmt From 9932d0cb5ccc9bfc49ee4c7383f215fec06e4b6a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:11 +0200 Subject: [PATCH 113/220] Sort dependencies --- .github/workflows/test-cygwin.yml | 28 ++++++++++++++++++++++------ .github/workflows/test-mingw.yml | 10 +++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7b8070d34..1dfb36f44 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -34,18 +34,34 @@ jobs: with: platform: x86_64 packages: > - ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel - libimagequant-devel libjpeg-devel liblapack-devel - liblcms2-devel libopenjp2-devel libraqm-devel - libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 - make netpbm perl + gcc-g++ + ghostscript + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools subversion xorg-server-extra zlib-devel + qt5-devel-tools + subversion + xorg-server-extra + zlib-devel - name: Add Lapack to PATH uses: egor-tensin/cleanup-path@v3 diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6a60bc7f0..24575f6c7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,11 +45,6 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ @@ -60,6 +55,11 @@ jobs: ${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libwebp \ ${{ matrix.package }}-openjpeg2 \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-setuptools \ subversion if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then From c8966013bd47fe59f729969463360f12763641c4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 27 Jan 2023 23:19:03 +0200 Subject: [PATCH 114/220] Replace SVN with Git for installing extra test images --- .appveyor.yml | 4 +++- .github/workflows/test-windows.yml | 5 ++++- depends/install_extra_test_images.sh | 21 +++++++++------------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b817cd9d8..d4dd2dc95 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,9 +21,11 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip +- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-depends.zip -oc:\ +- 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends -- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images +- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - ..\pillow-depends\gs1000w32.exe /S - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 487c3586f..48825dc30 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -62,7 +62,10 @@ jobs: winbuild\depends\gs1000w32.exe /S echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH - xcopy /S /Y winbuild\depends\test_images\* Tests\images\ + # Install extra test images + curl -fsSL -o pillow-test-images.zip https://github.com/hugovk/test-images/archive/main.zip + 7z x pillow-test-images.zip -oc:\ + xcopy /S /Y c:\test-images-main\* Tests\images\ # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 02da12d61..7381d3767 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,15 +1,12 @@ -#!/bin/bash +#!/usr/bin/env bash # install extra test images -# Use SVN to just fetch a single Git subdirectory -svn_export() -{ - if [ ! -z $1 ]; then - echo "" - echo "Retrying svn export..." - echo "" - fi +archive=test-images-main - svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images -} -svn_export || svn_export retry || svn_export retry || svn_export retry +./download-and-extract.sh $archive https://github.com/hugovk/test-images/archive/refs/heads/main.tar.gz + +mv $archive/* ../Tests/images/ + +# Cleanup old tarball and empty directory +rm $archive.tar.gz +rm -r $archive From 120d56b4ba49871a6d3032b7d0d4c8159e8273ec Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:11 +0200 Subject: [PATCH 115/220] Sort dependencies --- .github/workflows/test-cygwin.yml | 28 ++++++++++++++++++++++------ .github/workflows/test-mingw.yml | 10 +++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7b8070d34..1dfb36f44 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -34,18 +34,34 @@ jobs: with: platform: x86_64 packages: > - ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel - libimagequant-devel libjpeg-devel liblapack-devel - liblcms2-devel libopenjp2-devel libraqm-devel - libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 - make netpbm perl + gcc-g++ + ghostscript + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools subversion xorg-server-extra zlib-devel + qt5-devel-tools + subversion + xorg-server-extra + zlib-devel - name: Add Lapack to PATH uses: egor-tensin/cleanup-path@v3 diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6a60bc7f0..24575f6c7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,11 +45,6 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ @@ -60,6 +55,11 @@ jobs: ${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libwebp \ ${{ matrix.package }}-openjpeg2 \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-setuptools \ subversion if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then From 7e35e15eeeca12a4eec61d39ff7fc819d08d3554 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:25 +0200 Subject: [PATCH 116/220] Replace subversion with wget package --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 1dfb36f44..451181434 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -59,7 +59,7 @@ jobs: python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter qt5-devel-tools - subversion + wget xorg-server-extra zlib-devel diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 24575f6c7..ef8214649 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,7 +60,7 @@ jobs: ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ ${{ matrix.package }}-python3-setuptools \ - subversion + ${{ matrix.package }}-wget if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ From 772567a4ce9ca1890ecbee976bb777e82db2577a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 20:13:28 +1100 Subject: [PATCH 117/220] Switched to python-pillow repositories --- .github/workflows/test-windows.yml | 2 +- depends/install_extra_test_images.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 48825dc30..cf160a997 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -63,7 +63,7 @@ jobs: echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH # Install extra test images - curl -fsSL -o pillow-test-images.zip https://github.com/hugovk/test-images/archive/main.zip + curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip 7z x pillow-test-images.zip -oc:\ xcopy /S /Y c:\test-images-main\* Tests\images\ diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 7381d3767..ffdfe17f2 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -3,7 +3,7 @@ archive=test-images-main -./download-and-extract.sh $archive https://github.com/hugovk/test-images/archive/refs/heads/main.tar.gz +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/refs/heads/main.tar.gz mv $archive/* ../Tests/images/ From a119b19c074869267f186051af0d1b0878f9cca0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Feb 2023 08:36:06 +1100 Subject: [PATCH 118/220] Updated libjpeg-turbo to 2.1.5 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index fd12240e5..89903c621 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download", - "filename": "libjpeg-turbo-2.1.4.tar.gz", - "dir": "libjpeg-turbo-2.1.4", + + "/libjpeg-turbo/files/2.1.5/libjpeg-turbo-2.1.5.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.tar.gz", + "dir": "libjpeg-turbo-2.1.5", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From b3af769c1a85e62600d6bbf38a9e66639a60d43c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Feb 2023 20:52:50 +1100 Subject: [PATCH 119/220] 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 120/220] Added JPXDecode for RGBA images --- Tests/test_file_pdf.py | 7 ++++++- src/PIL/PdfImagePlugin.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 5299febe9..705505e83 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ import pytest from PIL import Image, PdfParser, features -from .helper import hopper, mark_if_feature_version +from .helper import hopper, mark_if_feature_version, skip_unless_feature def helper_save_as_pdf(tmp_path, mode, **kwargs): @@ -42,6 +42,11 @@ def test_save(tmp_path, mode): helper_save_as_pdf(tmp_path, mode) +@skip_unless_feature("jpg_2000") +def test_save_rgba(tmp_path): + helper_save_as_pdf(tmp_path, "RGBA") + + def test_monochrome(tmp_path): # Arrange mode = "1" diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index baad4939f..0b2938d6f 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -168,6 +168,10 @@ def _save(im, fp, filename, save_all=False): filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images + elif im.mode == "RGBA": + filter = "JPXDecode" + colorspace = PdfParser.PdfName("DeviceRGB") + procset = "ImageC" # color images elif im.mode == "CMYK": filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceCMYK") @@ -194,6 +198,8 @@ def _save(im, fp, filename, save_all=False): ) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) + elif filter == "JPXDecode": + Image.SAVE["JPEG2000"](im, op, filename) elif filter == "FlateDecode": ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) elif filter == "RunLengthDecode": From cc71b4c1b2226330c77210f2243deaa8b254afdc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 4 Feb 2023 10:04:29 +0200 Subject: [PATCH 121/220] Simpler URL Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/install_extra_test_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index ffdfe17f2..941bfbe84 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -3,7 +3,7 @@ archive=test-images-main -./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/refs/heads/main.tar.gz +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz mv $archive/* ../Tests/images/ From cb4eb0d40ff0f9b6a6f3deb2c299ed477c1afc00 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:25:53 +0000 Subject: [PATCH 122/220] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5214d352d..45c1f3c5f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black args: [--target-version=py37] From 24183d652e02404e1586ff00f7fe4a055b05ce58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:27:15 +0000 Subject: [PATCH 123/220] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/check_fli_overflow.py | 1 - Tests/check_png_dos.py | 1 - Tests/test_bmp_reference.py | 1 - Tests/test_file_bmp.py | 1 - Tests/test_file_bufrstub.py | 2 -- Tests/test_file_dcx.py | 2 -- Tests/test_file_eps.py | 1 - Tests/test_file_fits.py | 1 - Tests/test_file_fli.py | 2 -- Tests/test_file_gif.py | 13 +------------ Tests/test_file_gribstub.py | 2 -- Tests/test_file_hdf5stub.py | 2 -- Tests/test_file_icns.py | 1 - Tests/test_file_ico.py | 1 - Tests/test_file_im.py | 1 - Tests/test_file_iptc.py | 3 --- Tests/test_file_jpeg.py | 13 ------------- Tests/test_file_jpeg2k.py | 1 - Tests/test_file_libtiff.py | 2 -- Tests/test_file_msp.py | 2 -- Tests/test_file_pdf.py | 2 -- Tests/test_file_png.py | 4 ---- Tests/test_file_psd.py | 2 -- Tests/test_file_spider.py | 1 - Tests/test_file_sun.py | 1 - Tests/test_file_tga.py | 3 --- Tests/test_file_tiff.py | 7 ------- Tests/test_file_tiff_metadata.py | 3 --- Tests/test_file_webp_metadata.py | 4 ---- Tests/test_file_wmf.py | 1 - Tests/test_file_xbm.py | 2 -- Tests/test_file_xvthumb.py | 1 - Tests/test_image.py | 2 -- Tests/test_image_convert.py | 1 - Tests/test_image_crop.py | 1 - Tests/test_image_getbbox.py | 1 - Tests/test_image_mode.py | 1 - Tests/test_image_rotate.py | 2 +- Tests/test_image_tobitmap.py | 1 - Tests/test_imagechops.py | 21 --------------------- Tests/test_imagedraw.py | 1 - Tests/test_imagefile.py | 1 - Tests/test_imageops.py | 3 --- Tests/test_imagepalette.py | 3 --- Tests/test_imagepath.py | 1 - Tests/test_imagesequence.py | 1 - Tests/test_imagestat.py | 3 --- Tests/test_lib_image.py | 1 - Tests/test_mode_i16.py | 2 -- Tests/test_numpy.py | 1 - Tests/test_pickle.py | 1 - Tests/test_tiff_ifdrational.py | 2 -- Tests/test_webp_leaks.py | 1 - setup.py | 2 -- src/PIL/BmpImagePlugin.py | 2 -- src/PIL/BufrStubImagePlugin.py | 2 -- src/PIL/CurImagePlugin.py | 2 -- src/PIL/DcxImagePlugin.py | 2 -- src/PIL/EpsImagePlugin.py | 2 -- src/PIL/FitsImagePlugin.py | 1 - src/PIL/FitsStubImagePlugin.py | 1 - src/PIL/FliImagePlugin.py | 2 -- src/PIL/FontFile.py | 1 - src/PIL/FpxImagePlugin.py | 5 ----- src/PIL/GbrImagePlugin.py | 1 - src/PIL/GdImageFile.py | 1 - src/PIL/GifImagePlugin.py | 6 ------ src/PIL/GimpGradientFile.py | 5 ----- src/PIL/GimpPaletteFile.py | 3 --- src/PIL/GribStubImagePlugin.py | 2 -- src/PIL/Hdf5StubImagePlugin.py | 2 -- src/PIL/IcnsImagePlugin.py | 3 +-- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 7 ------- src/PIL/Image.py | 3 +-- src/PIL/ImageDraw.py | 4 ++-- src/PIL/ImageFile.py | 3 --- src/PIL/ImageFont.py | 2 -- src/PIL/ImageOps.py | 2 -- src/PIL/ImagePalette.py | 2 -- src/PIL/ImageShow.py | 1 - src/PIL/ImageTk.py | 2 -- src/PIL/ImtImagePlugin.py | 5 ----- src/PIL/IptcImagePlugin.py | 3 --- src/PIL/JpegImagePlugin.py | 5 ----- src/PIL/McIdasImagePlugin.py | 2 -- src/PIL/MicImagePlugin.py | 2 -- src/PIL/MpegImagePlugin.py | 2 -- src/PIL/MpoImagePlugin.py | 1 - src/PIL/MspImagePlugin.py | 4 ---- src/PIL/PaletteFile.py | 3 --- src/PIL/PalmImagePlugin.py | 4 ---- src/PIL/PcdImagePlugin.py | 2 -- src/PIL/PcfFontFile.py | 7 ------- src/PIL/PcxImagePlugin.py | 3 --- src/PIL/PixarImagePlugin.py | 2 -- src/PIL/PngImagePlugin.py | 17 ----------------- src/PIL/PpmImagePlugin.py | 1 - src/PIL/PsdImagePlugin.py | 4 ---- src/PIL/SgiImagePlugin.py | 2 -- src/PIL/SpiderImagePlugin.py | 3 +-- src/PIL/SunImagePlugin.py | 2 -- src/PIL/TarIO.py | 1 - src/PIL/TgaImagePlugin.py | 3 --- src/PIL/TiffImagePlugin.py | 4 ---- src/PIL/WalImageFile.py | 1 - src/PIL/WebPImagePlugin.py | 1 - src/PIL/WmfImagePlugin.py | 2 -- src/PIL/XVThumbImagePlugin.py | 2 -- src/PIL/XbmImagePlugin.py | 3 --- src/PIL/XpmImagePlugin.py | 7 ------- 111 files changed, 8 insertions(+), 298 deletions(-) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 08a55d349..c600c45ed 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -4,7 +4,6 @@ TEST_FILE = "Tests/images/fli_overflow.fli" def test_fli_overflow(): - # this should not crash with a malloc error or access violation with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index d8d645189..f4a129f50 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -23,7 +23,6 @@ def test_ignore_dos_text(): def test_dos_text(): - try: im = Image.open(TEST_FILE) im.load() diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index ed9aff9cc..002a44a4f 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -18,7 +18,6 @@ def test_bad(): """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): - # Assert that there is no unclosed file warning with warnings.catch_warnings(): try: diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 5f6d52355..9e79937e9 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -141,7 +141,6 @@ def test_rgba_bitfields(): # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: - # So before the comparing the image, swap the channels b, g, r = im.split()[1:] im = Image.merge("RGB", (r, g, b)) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index e330404d6..76f185b9a 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "BUFR" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 0f09c4b99..ef378b24a 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -15,7 +15,6 @@ def test_sanity(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.size == (128, 128) assert isinstance(im, DcxImagePlugin.DcxImageFile) @@ -54,7 +53,6 @@ def test_invalid_file(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 015dda992..ac6e84447 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -80,7 +80,6 @@ def test_invalid_file(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 447888acd..d2f5a6d17 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -12,7 +12,6 @@ TEST_FILE = "Tests/images/hopper.fits" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "FITS" assert im.size == (128, 128) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index b8b999d70..70d4d76db 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -64,7 +64,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(static_test_file) as im: - # Act frame = im.tell() @@ -110,7 +109,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(animated_test_file) as im: - layer_number = im.tell() assert layer_number == 0 diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6fbc0ee30..bce72d192 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -209,7 +209,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): im = im.resize((591, 443)) im_rgb = im.convert("RGB") - for (optimize, colors) in ((False, 256), (True, 8)): + for optimize, colors in ((False, 256), (True, 8)): out = BytesIO() im_rgb.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: @@ -221,7 +221,6 @@ def test_roundtrip(tmp_path): im = hopper() im.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) @@ -232,7 +231,6 @@ def test_roundtrip2(tmp_path): im2 = im.copy() im2.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), hopper(), 50) @@ -242,7 +240,6 @@ def test_roundtrip_save_all(tmp_path): im = hopper() im.save(out, save_all=True) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) # Multiframe image @@ -284,13 +281,11 @@ def test_headers_saving_for_animated_gifs(tmp_path): important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - info = im.info.copy() out = str(tmp_path / "temp.gif") im.save(out, save_all=True) with Image.open(out) as reread: - for header in important_headers: assert info[header] == reread.info[header] @@ -308,7 +303,6 @@ def test_palette_handling(tmp_path): im2.save(f, optimize=True) with Image.open(f) as reloaded: - assert_image_similar(im, reloaded.convert("RGB"), 10) @@ -324,7 +318,6 @@ def test_palette_434(tmp_path): orig = "Tests/images/test.colors.gif" with Image.open(orig) as im: - with roundtrip(im) as reloaded: assert_image_similar(im, reloaded, 1) with roundtrip(im, optimize=True) as reloaded: @@ -575,7 +568,6 @@ def test_save_dispose(tmp_path): ) with Image.open(out) as img: - for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -773,7 +765,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -786,7 +777,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -844,7 +834,6 @@ def test_identical_frames(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - # Assert that the first three frames were combined assert reread.n_frames == 2 diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index fd427746e..768ac12bd 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "GRIB" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 20b4b9619..98dc5443c 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -8,7 +8,6 @@ TEST_FILE = "Tests/images/hdf5.h5" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "HDF5" @@ -29,7 +28,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 55632909c..42275424d 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -16,7 +16,6 @@ def test_sanity(): # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning with warnings.catch_warnings(): im.load() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index afb17b1af..9c1c3cf17 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -175,7 +175,6 @@ def test_save_256x256(tmp_path): # Act im.save(outfile) with Image.open(outfile) as im_saved: - # Assert assert im_saved.size == (256, 256) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 5cf93713b..425e690d6 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -51,7 +51,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(TEST_IM) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 2d0e6977a..2d99528d3 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,7 +11,6 @@ TEST_FILE = "Tests/images/iptc.jpg" def test_getiptcinfo_jpg_none(): # Arrange with hopper() as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -22,7 +21,6 @@ def test_getiptcinfo_jpg_none(): def test_getiptcinfo_jpg_found(): # Arrange with Image.open(TEST_FILE) as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -35,7 +33,6 @@ def test_getiptcinfo_jpg_found(): def test_getiptcinfo_tiff_none(): # Arrange with Image.open("Tests/images/hopper.tif") as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..e3c5abcbd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -57,7 +57,6 @@ class TestFileJpeg: return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) def test_sanity(self): - # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -368,7 +367,6 @@ class TestFileJpeg: def test_exif_gps_typeerror(self): with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: - # Should not raise a TypeError im._getexif() @@ -682,7 +680,6 @@ class TestFileJpeg: # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert assert im.format == "JPEG" @@ -704,7 +701,6 @@ class TestFileJpeg: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: - # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -731,7 +727,6 @@ class TestFileJpeg: # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (200, 200) @@ -740,7 +735,6 @@ class TestFileJpeg: # This image has DPI in EXIF not metadata # EXIF XResolution is 72 with Image.open("Tests/images/exif-72dpi-int.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (72, 72) @@ -749,7 +743,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg with Image.open("Tests/images/exif-200dpcm.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (508, 508) @@ -758,7 +751,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: - # Act / Assert # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) @@ -767,7 +759,6 @@ class TestFileJpeg: # Arrange # 0x011A tag in this exif contains string '300300\x02' with Image.open("Tests/images/broken_exif_dpi.jpg") as im: - # Act / Assert # This should return the default assert im.info.get("dpi") == (72, 72) @@ -777,7 +768,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: - # Act / Assert # "When the image resolution is unknown, 72 [dpi] is designated." # https://exiv2.org/tags.html @@ -787,7 +777,6 @@ class TestFileJpeg: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: - # This should return the default, and not a SyntaxError or # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) @@ -810,7 +799,6 @@ class TestFileJpeg: def test_invalid_exif_x_resolution(self): # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: - # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) @@ -820,7 +808,6 @@ class TestFileJpeg: # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 with Image.open("Tests/images/exif-ifd-offset.jpg") as im: - # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0229b2243..de622c478 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -270,7 +270,6 @@ def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act j2k.load() jp2.load() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1109cd15e..f886d3aae 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -645,7 +645,6 @@ class TestFileLibTiff(LibTiffTestCase): pilim = hopper() def save_bytesio(compression=None): - buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -740,7 +739,6 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self): with Image.open("Tests/images/compression.tif") as im: - im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 50d7c590b..497052b05 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -44,7 +44,6 @@ def test_open_windows_v1(): # Arrange # Act with Image.open(TEST_FILE) as im: - # Assert assert_image_equal(im, hopper("1")) assert isinstance(im, MspImagePlugin.MspImageFile) @@ -59,7 +58,6 @@ def _assert_file_image_equal(source_path, target_path): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) def test_open_windows_v2(): - files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 5299febe9..216b93ca9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -89,7 +89,6 @@ def test_save_all(tmp_path): # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile, save_all=True) @@ -123,7 +122,6 @@ def test_save_all(tmp_path): def test_multiframe_normal_save(tmp_path): # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 133f3e47e..c4db97905 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -78,7 +78,6 @@ class TestFilePng: return chunks def test_sanity(self, tmp_path): - # internal version number assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) @@ -156,7 +155,6 @@ class TestFilePng: assert im.info == {"spam": "egg"} def test_bad_itxt(self): - im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -201,7 +199,6 @@ class TestFilePng: assert im.info["spam"].tkey == "Spam" def test_interlace(self): - test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -495,7 +492,6 @@ class TestFilePng: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: - assert im.info["transparency"] == 0 def test_save_icc_profile(self): diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 4f934375c..036cb9d4b 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -77,7 +77,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(test_file) as im: - layer_number = im.tell() assert layer_number == 1 @@ -95,7 +94,6 @@ def test_seek_tell(): def test_seek_eoferror(): with Image.open(test_file) as im: - with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 0e3b705a2..011e208d8 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -79,7 +79,6 @@ def test_is_spider_image(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act index = im.tell() diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 05c78c316..edb320603 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -16,7 +16,6 @@ def test_sanity(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (128, 128) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 7d8b5139a..bac00e855 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -78,7 +78,6 @@ def test_id_field(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (100, 100) @@ -89,7 +88,6 @@ def test_id_field_rle(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (199, 199) @@ -171,7 +169,6 @@ def test_save_id_section(tmp_path): test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: - # Save with no id section im.save(out, id_section="") with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 4f3c8e390..70142747c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -25,7 +25,6 @@ except ImportError: class TestFileTiff: def test_sanity(self, tmp_path): - filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -157,7 +156,6 @@ class TestFileTiff: def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -171,7 +169,6 @@ class TestFileTiff: def test_xyres_fallback_tiff(self): filename = "Tests/images/compression.tif" with Image.open(filename) as im: - # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -186,7 +183,6 @@ class TestFileTiff: def test_int_resolution(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -381,7 +377,6 @@ class TestFileTiff: def test___str__(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # Act ret = str(im.ifd) @@ -392,7 +387,6 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # v2 interface v2_tags = { 256: 55, @@ -630,7 +624,6 @@ class TestFileTiff: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) with Image.open(filename) as im: - # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1061f7d05..a4481d85f 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -54,7 +54,6 @@ def test_rt_metadata(tmp_path): img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -74,14 +73,12 @@ def test_rt_metadata(tmp_path): info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata(): with Image.open("Tests/images/hopper_g4.tif") as img: - assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 4f513d82b..037479f9f 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -18,10 +18,8 @@ except ImportError: def test_read_exif_metadata(): - file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: - assert image.format == "WEBP" exif_data = image.info.get("exif", None) assert exif_data @@ -64,10 +62,8 @@ def test_write_exif_metadata(): def test_read_icc_profile(): - file_path = "Tests/images/flower2.webp" with Image.open(file_path) as image: - assert image.format == "WEBP" assert image.info.get("icc_profile", None) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 439cb15bc..7c8b54fd1 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -6,7 +6,6 @@ from .helper import assert_image_similar_tofile, hopper def test_load_raw(): - # Test basic EMF open and rendering with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 9c54c6755..d2c05b78a 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -44,7 +44,6 @@ def test_open(): # Act with Image.open(filename) as im: - # Assert assert im.mode == "1" assert im.size == (128, 128) @@ -57,7 +56,6 @@ def test_open_filename_with_underscore(): # Act with Image.open(filename) as im: - # Assert assert im.mode == "1" assert im.size == (128, 128) diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index ae53d2b63..9efe7ec14 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/hopper.p7" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "XVThumb" diff --git a/Tests/test_image.py b/Tests/test_image.py index ad3346b5a..85e3ff55b 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -69,7 +69,6 @@ class TestImage: assert issubclass(UnidentifiedImageError, OSError) def test_sanity(self): - im = Image.new("L", (100, 100)) assert repr(im)[:45] == ">> im.save('Tests/images/hopper_45.png') with Image.open("Tests/images/hopper_45.png") as target: - for (resample, epsilon) in ( + for resample, epsilon in ( (Image.Resampling.NEAREST, 10), (Image.Resampling.BILINEAR, 5), (Image.Resampling.BICUBIC, 0), diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 178cfcef3..a12ce329f 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -4,7 +4,6 @@ from .helper import assert_image_equal, fromstring, hopper def test_sanity(): - with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index b839a7b14..d0fea3854 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -50,7 +50,6 @@ def test_add(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2) @@ -63,7 +62,6 @@ def test_add_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2, scale=2.5, offset=100) @@ -87,7 +85,6 @@ def test_add_modulo(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add_modulo(im1, im2) @@ -111,7 +108,6 @@ def test_blend(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.blend(im1, im2, 0.5) @@ -137,7 +133,6 @@ def test_darker_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -149,7 +144,6 @@ def test_darker_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -161,7 +155,6 @@ def test_difference(): # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -173,7 +166,6 @@ def test_difference_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -195,7 +187,6 @@ def test_duplicate(): def test_invert(): # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: - # Act new = ImageChops.invert(im) @@ -209,7 +200,6 @@ def test_lighter_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -221,7 +211,6 @@ def test_lighter_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -275,7 +264,6 @@ def test_offset(): xoffset = 45 yoffset = 20 with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: - # Act new = ImageChops.offset(im, xoffset, yoffset) @@ -292,7 +280,6 @@ def test_screen(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.screen(im1, im2) @@ -305,7 +292,6 @@ def test_subtract(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -319,7 +305,6 @@ def test_subtract_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) @@ -332,7 +317,6 @@ def test_subtract_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -344,7 +328,6 @@ def test_subtract_modulo(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -358,7 +341,6 @@ def test_subtract_modulo_no_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -370,7 +352,6 @@ def test_soft_light(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.soft_light(im1, im2) @@ -383,7 +364,6 @@ def test_hard_light(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.hard_light(im1, im2) @@ -396,7 +376,6 @@ def test_overlay(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.overlay(im1, im2) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4c4c41b7b..d4723c924 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -52,7 +52,6 @@ def test_sanity(): def test_valueerror(): with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fc0fbfb9b..412bc10d9 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,7 +30,6 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index c9b2fd865..d390f3c1e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -21,7 +21,6 @@ deformer = Deformer() def test_sanity(): - ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -419,7 +418,6 @@ def test_autocontrast_cutoff(): def test_autocontrast_mask_toy_input(): # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0 = img.size[0] // 4 @@ -439,7 +437,6 @@ def test_autocontrast_mask_toy_input(): def test_autocontrast_mask_real_input(): # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0, y0 = img.size[0] // 2, img.size[1] // 2 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 5bda28117..ac99ef381 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, assert_image_equal_tofile def test_sanity(): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 @@ -23,7 +22,6 @@ def test_reload(): def test_getcolor(): - palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -84,7 +82,6 @@ def test_getcolor_not_special(index, palette): def test_file(tmp_path): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 861fb64f0..8f8a9f449 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -8,7 +8,6 @@ from PIL import Image, ImagePath def test_path(): - p = ImagePath.Path(list(range(10))) # sequence interface diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6af7e7602..62f528332 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, hopper, skip_unless_feature def test_sanity(tmp_path): - test_file = str(tmp_path / "temp.im") im = hopper("RGB") diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 5717fe150..b3b5db13f 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -6,7 +6,6 @@ from .helper import hopper def test_sanity(): - im = hopper() st = ImageStat.Stat(im) @@ -31,7 +30,6 @@ def test_sanity(): def test_hopper(): - im = hopper() st = ImageStat.Stat(im) @@ -45,7 +43,6 @@ def test_hopper(): def test_constant(): - im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 37ed3659d..f6818be46 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -4,7 +4,6 @@ from PIL import Image def test_setmode(): - im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index efcdab9ec..dcdee3d41 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -42,7 +42,6 @@ def test_basic(tmp_path, mode): im_in.save(filename) with Image.open(filename) as im_out: - verify(im_in) verify(im_out) @@ -87,7 +86,6 @@ def test_tobytes(): def test_convert(): - im = original.copy() verify(im.convert("I;16")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 3de7ec30f..a8bbcbdb8 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -235,7 +235,6 @@ def test_no_resource_warning_for_numpy_array(): test_file = "Tests/images/hopper.png" with Image.open(test_file) as im: - # Act/Assert with warnings.catch_warnings(): array(im) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 23eb9e39f..2f6d05888 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -89,7 +89,6 @@ def test_pickle_la_mode_with_palette(tmp_path): def test_pickle_tell(): # Arrange with Image.open("Tests/images/hopper.webp") as image: - # Act: roundtrip unpickled_image = pickle.loads(pickle.dumps(image)) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 12f475df0..6e3fcec90 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -7,7 +7,6 @@ from .helper import hopper def _test_equal(num, denom, target): - t = IFDRational(num, denom) assert target == t @@ -15,7 +14,6 @@ def _test_equal(num, denom, target): def test_sanity(): - _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 34197c14f..5bd9bacdb 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -9,7 +9,6 @@ test_file = "Tests/images/hopper.webp" @skip_unless_feature("webp") class TestWebPLeaks(PillowLeakTestCase): - mem_limit = 3 * 1024 # kb iterations = 100 diff --git a/setup.py b/setup.py index 4382c1a97..8f7f223f8 100755 --- a/setup.py +++ b/setup.py @@ -430,7 +430,6 @@ class pil_build_ext(build_ext): return sdk_path def build_extensions(self): - library_dirs = [] include_dirs = [] @@ -917,7 +916,6 @@ class pil_build_ext(build_ext): self.summary_report(feature) def summary_report(self, feature): - print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index e13b18f27..5bda0a5b0 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -223,7 +223,6 @@ class BmpImageFile(ImageFile.ImageFile): # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images - # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): msg = f"Unsupported BMP Palette size ({file_info['colors']})" @@ -360,7 +359,6 @@ class BmpRleDecoder(ImageFile.PyDecoder): # Image plugin for the DIB format (BMP alias) # ============================================================================= class DibImageFile(BmpImageFile): - format = "DIB" format_description = "Windows Bitmap" diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index a0da1b786..0425bbd75 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class BufrStubImageFile(ImageFile.StubImageFile): - format = "BUFR" format_description = "BUFR" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(4)): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index aedc6ce7f..94efff341 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -32,12 +32,10 @@ def _accept(prefix): class CurImageFile(BmpImagePlugin.BmpImageFile): - format = "CUR" format_description = "Windows Cursor" def _open(self): - offset = self.fp.tell() # check magic diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 81c0314f0..cde9d42f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -37,13 +37,11 @@ def _accept(prefix): class DcxImageFile(PcxImageFile): - format = "DCX" format_description = "Intel DCX" _close_exclusive_fp_after_loading = False def _open(self): - # Header s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index dd68c13e5..60cb46df9 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -286,7 +286,6 @@ class EpsImageFile(ImageFile.ImageFile): # Scan for an "ImageData" descriptor while s[:1] == "%": - if len(s) > 255: msg = "not an EPS file" raise SyntaxError(msg) @@ -317,7 +316,6 @@ class EpsImageFile(ImageFile.ImageFile): raise OSError(msg) def _find_offset(self, fp): - s = fp.read(4) if s == b"%!PS": diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 536bc1fe6..1185ef2d3 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -19,7 +19,6 @@ def _accept(prefix): class FitsImageFile(ImageFile.ImageFile): - format = "FITS" format_description = "FITS" diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 86eb2d5a2..50948ec42 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -44,7 +44,6 @@ def register_handler(handler): class FITSStubImageFile(ImageFile.StubImageFile): - format = FitsImagePlugin.FitsImageFile.format format_description = FitsImagePlugin.FitsImageFile.format_description diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 66681939d..f4e89a03e 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -40,13 +40,11 @@ def _accept(prefix): class FliImageFile(ImageFile.ImageFile): - format = "FLI" format_description = "Autodesk FLI/FLC Animation" _close_exclusive_fp_after_loading = False def _open(self): - # HEAD s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index c5fc80b37..5ec0a6632 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -36,7 +36,6 @@ class FontFile: bitmap = None def __init__(self): - self.info = {} self.glyph = [None] * 256 diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8ddc6b40b..d145d01f7 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -48,7 +48,6 @@ def _accept(prefix): class FpxImageFile(ImageFile.ImageFile): - format = "FPX" format_description = "FlashPix" @@ -157,7 +156,6 @@ class FpxImageFile(ImageFile.ImageFile): self.tile = [] for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) y1 = min(ysize, y + ytile) @@ -174,7 +172,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 1: - # FIXME: the fill decoder is not implemented self.tile.append( ( @@ -186,7 +183,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 2: - internal_color_conversion = s[14] jpeg_tables = s[15] rawmode = self.rawmode @@ -234,7 +230,6 @@ class FpxImageFile(ImageFile.ImageFile): self.fp = None def load(self): - if not self.fp: self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 828a45ced..994a6e8eb 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -37,7 +37,6 @@ def _accept(prefix): class GbrImageFile(ImageFile.ImageFile): - format = "GBR" format_description = "GIMP brush file" diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 3875dc866..7dda4f143 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -44,7 +44,6 @@ class GdImageFile(ImageFile.ImageFile): format_description = "GD uncompressed images" def _open(self): - # Header s = self.fp.read(1037) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 6ee1bd3d8..eadee1560 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -61,7 +61,6 @@ def _accept(prefix): class GifImageFile(ImageFile.ImageFile): - format = "GIF" format_description = "Compuserve GIF" _close_exclusive_fp_after_loading = False @@ -81,7 +80,6 @@ class GifImageFile(ImageFile.ImageFile): return False def _open(self): - # Screen s = self.fp.read(13) if not _accept(s): @@ -157,7 +155,6 @@ class GifImageFile(ImageFile.ImageFile): raise EOFError(msg) from e def _seek(self, frame, update_image=True): - if frame == 0: # rewind self.__offset = 0 @@ -195,7 +192,6 @@ class GifImageFile(ImageFile.ImageFile): interlace = None frame_dispose_extent = None while True: - if not s: s = self.fp.read(1) if not s or s == b";": @@ -579,7 +575,6 @@ def _getbbox(base_im, im_frame): def _write_multiple_frames(im, fp, palette): - duration = im.encoderinfo.get("duration") disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) @@ -752,7 +747,6 @@ def _write_local_header(fp, im, offset, flags): def _save_netpbm(im, fp, filename): - # Unused by default. # To use, uncomment the register_save call at the end of the file. # diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index b5c5e3ca4..8e801be0b 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -64,18 +64,15 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] class GradientFile: - gradient = None def getpalette(self, entries=256): - palette = [] ix = 0 x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] for i in range(entries): - x = i / (entries - 1) while x1 < x: @@ -105,7 +102,6 @@ class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" def __init__(self, fp): - if fp.readline()[:13] != b"GIMP Gradient": msg = "not a GIMP gradient file" raise SyntaxError(msg) @@ -121,7 +117,6 @@ class GimpGradientFile(GradientFile): gradient = [] for i in range(count): - s = fp.readline().split() w = [float(x) for x in s[:11]] diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 2e9cbe58d..d38892894 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -25,7 +25,6 @@ class GimpPaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": @@ -33,7 +32,6 @@ class GimpPaletteFile: raise SyntaxError(msg) for i in range(256): - s = fp.readline() if not s: break @@ -55,5 +53,4 @@ class GimpPaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 2088eb7b0..8a799f19c 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class GribStubImageFile(ImageFile.StubImageFile): - format = "GRIB" format_description = "GRIB" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index d6f283739..bba05ed65 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class HDF5StubImageFile(ImageFile.StubImageFile): - format = "HDF5" format_description = "HDF5" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index e76d0c35a..c2f050edd 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -135,7 +135,6 @@ def read_png_or_jpeg2000(fobj, start_length, size): class IcnsFile: - SIZES = { (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], @@ -189,7 +188,7 @@ class IcnsFile: def itersizes(self): sizes = [] for size, fmts in self.SIZES.items(): - for (fmt, reader) in fmts: + for fmt, reader in fmts: if fmt in self.dct: sizes.append(size) break diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 568e6d38d..a188f8fdc 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -185,7 +185,7 @@ class IcoFile: return {(h["width"], h["height"]) for h in self.entry} def getentryindex(self, size, bpp=False): - for (i, h) in enumerate(self.entry): + for i, h in enumerate(self.entry): if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): return i return 0 diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 875a20326..746743f65 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -115,13 +115,11 @@ def number(s): class ImImageFile(ImageFile.ImageFile): - format = "IM" format_description = "IFUNC Image Memory" _close_exclusive_fp_after_loading = False def _open(self): - # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. @@ -140,7 +138,6 @@ class ImImageFile(ImageFile.ImageFile): self.rawmode = "L" while True: - s = self.fp.read(1) # Some versions of IFUNC uses \n\r instead of \r\n... @@ -169,7 +166,6 @@ class ImImageFile(ImageFile.ImageFile): raise SyntaxError(msg) from e if m: - k, v = m.group(1, 2) # Don't know if this is the correct encoding, @@ -200,7 +196,6 @@ class ImImageFile(ImageFile.ImageFile): n += 1 else: - msg = "Syntax error in IM header: " + s.decode("ascii", "replace") raise SyntaxError(msg) @@ -252,7 +247,6 @@ class ImImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": - # ifunc95 formats try: # use bit decoder (if necessary) @@ -332,7 +326,6 @@ SAVE = { def _save(im, fp, filename): - try: image_type, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ad0d25add..81123d070 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -153,6 +153,7 @@ def isImageType(t): # # Constants + # transpose class Transpose(IntEnum): FLIP_LEFT_RIGHT = 0 @@ -391,7 +392,6 @@ def init(): def _getdecoder(mode, decoder_name, args, extra=()): - # tweak arguments if args is None: args = () @@ -415,7 +415,6 @@ def _getdecoder(mode, decoder_name, args, extra=()): def _getencoder(mode, encoder_name, args, extra=()): - # tweak arguments if args is None: args = () diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ce29a163b..163828d31 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -928,8 +928,8 @@ def floodfill(image, xy, value, border=None, thresh=0): full_edge = set() while edge: new_edge = set() - for (x, y) in edge: # 4 adjacent method - for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + for x, y in edge: # 4 adjacent method + for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): # If already processed, or if a coordinate is negative, skip if (s, t) in full_edge or s < 0 or t < 0: continue diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 12391955f..132490a8e 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -395,7 +395,6 @@ class Parser: # parse what we have if self.decoder: - if self.offset > 0: # skip header skip = min(len(self.data), self.offset) @@ -420,14 +419,12 @@ class Parser: self.data = self.data[n:] elif self.image: - # if we end up here with no decoder, this file cannot # be incrementally parsed. wait until we've gotten all # available data pass else: - # attempt to open this file try: with io.BytesIO(self.data) as fp: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b144c3dd2..bd13c391e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -90,7 +90,6 @@ class ImageFont: """PIL font wrapper""" def _load_pilfont(self, filename): - with open(filename, "rb") as fp: image = None for ext in (".png", ".gif", ".pbm"): @@ -116,7 +115,6 @@ class ImageFont: image.close() def _load_pilfont_data(self, file, image): - # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 16c83f4e4..301c593c7 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -205,7 +205,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (2-color) if mid is None: - range_map = range(0, whitepoint - blackpoint) for i in range_map: @@ -215,7 +214,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (3-color) else: - range_map1 = range(0, midpoint - blackpoint) range_map2 = range(0, whitepoint - midpoint) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fe0d32155..e455c0459 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -248,11 +248,9 @@ def wedge(mode="RGB"): def load(filename): - # FIXME: supports GIMP gradients only with open(filename, "rb") as fp: - for paletteHandler in [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 29d900bef..f0e73fb90 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -390,7 +390,6 @@ else: if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 ImageShow.py imagefile [title]") sys.exit() diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 09a6356fa..ef569ed2e 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -97,7 +97,6 @@ class PhotoImage: """ def __init__(self, image=None, size=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) @@ -209,7 +208,6 @@ class BitmapImage: """ def __init__(self, image=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index cfeadd53c..ac267457b 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -30,12 +30,10 @@ field = re.compile(rb"([a-z]*) ([^ \r\n]*)") class ImtImageFile(ImageFile.ImageFile): - format = "IMT" format_description = "IM Tools" def _open(self): - # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. @@ -47,7 +45,6 @@ class ImtImageFile(ImageFile.ImageFile): xsize = ysize = 0 while True: - if buffer: s = buffer[:1] buffer = buffer[1:] @@ -57,7 +54,6 @@ class ImtImageFile(ImageFile.ImageFile): break if s == b"\x0C": - # image data begins self.tile = [ ( @@ -71,7 +67,6 @@ class ImtImageFile(ImageFile.ImageFile): break else: - # read key/value pair if b"\n" not in buffer: buffer += self.fp.read(100) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 774817569..4c47b55c1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -48,7 +48,6 @@ def dump(c): class IptcImageFile(ImageFile.ImageFile): - format = "IPTC" format_description = "IPTC/NAA" @@ -84,7 +83,6 @@ class IptcImageFile(ImageFile.ImageFile): return tag, size def _open(self): - # load descriptive fields while True: offset = self.fp.tell() @@ -134,7 +132,6 @@ class IptcImageFile(ImageFile.ImageFile): ] def load(self): - if len(self.tile) != 1 or self.tile[0][0] != "iptc": return ImageFile.ImageFile.load(self) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index b9c80236e..d7ddbe0d9 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -344,12 +344,10 @@ def _accept(prefix): class JpegImageFile(ImageFile.ImageFile): - format = "JPEG" format_description = "JPEG (ISO 10918)" def _open(self): - s = self.fp.read(3) if not _accept(s): @@ -370,7 +368,6 @@ class JpegImageFile(ImageFile.ImageFile): self.icclist = [] while True: - i = s[0] if i == 0xFF: s = s + self.fp.read(1) @@ -418,7 +415,6 @@ class JpegImageFile(ImageFile.ImageFile): return s def draft(self, mode, size): - if len(self.tile) != 1: return @@ -455,7 +451,6 @@ class JpegImageFile(ImageFile.ImageFile): return self.mode, box def load_djpeg(self): - # ALTERNATIVE: handle JPEGs via the IJG command line utilities f, path = tempfile.mkstemp() diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 8d4d826aa..17c008b9a 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -30,12 +30,10 @@ def _accept(s): class McIdasImageFile(ImageFile.ImageFile): - format = "MCIDAS" format_description = "McIdas area file" def _open(self): - # parse area file directory s = self.fp.read(256) if not _accept(s) or len(s) != 256: diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index e7e1054a3..8dd9f2909 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -34,13 +34,11 @@ def _accept(prefix): class MicImageFile(TiffImagePlugin.TiffImageFile): - format = "MIC" format_description = "Microsoft Image Composer" _close_exclusive_fp_after_loading = False def _open(self): - # read the OLE directory and see if this is a likely # to be a Microsoft Image Composer file diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 2d799d6d8..d96d3a11c 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -58,12 +58,10 @@ class BitStream: class MpegImageFile(ImageFile.ImageFile): - format = "MPEG" format_description = "MPEG" def _open(self): - s = BitStream(self.fp) if s.read(32) != 0x1B3: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index b1ec2c7bc..f9261c77d 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -101,7 +101,6 @@ def _save_all(im, fp, filename): class MpoImageFile(JpegImagePlugin.JpegImageFile): - format = "MPO" format_description = "MPO (CIPA DC-007)" _close_exclusive_fp_after_loading = False diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 5420894dc..c6567b2ae 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class MspImageFile(ImageFile.ImageFile): - format = "MSP" format_description = "Windows Paint" def _open(self): - # Header s = self.fp.read(32) if not _accept(s): @@ -111,7 +109,6 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True def decode(self, buffer): - img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -162,7 +159,6 @@ Image.register_decoder("MSP", MspDecoder) def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 07acd5580..4a2c497fc 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -22,11 +22,9 @@ class PaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [(i, i, i) for i in range(256)] while True: - s = fp.readline() if not s: @@ -50,5 +48,4 @@ class PaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 109aad9ab..a88a90791 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -112,9 +112,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} def _save(im, fp, filename): - if im.mode == "P": - # we assume this is a color Palm image with the standard colormap, # unless the "info" dict has a "custom-colormap" field @@ -147,14 +145,12 @@ def _save(im, fp, filename): version = 1 elif im.mode == "1": - # monochrome -- write it inverted, as is the Palm standard rawmode = "1;I" bpp = 1 version = 0 else: - msg = f"cannot write mode {im.mode} as Palm" raise OSError(msg) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 5802d386a..e390f3fe5 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -24,12 +24,10 @@ from . import Image, ImageFile class PcdImageFile(ImageFile.ImageFile): - format = "PCD" format_description = "Kodak PhotoCD" def _open(self): - # rough self.fp.seek(2048) s = self.fp.read(2048) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index ecce1b097..d5f510f03 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -58,7 +58,6 @@ class PcfFontFile(FontFile.FontFile): name = "name" def __init__(self, fp, charset_encoding="iso8859-1"): - self.charset_encoding = charset_encoding magic = l32(fp.read(4)) @@ -92,7 +91,6 @@ class PcfFontFile(FontFile.FontFile): self.glyph[ch] = glyph def _getformat(self, tag): - format, size, offset = self.toc[tag] fp = self.fp @@ -108,7 +106,6 @@ class PcfFontFile(FontFile.FontFile): return fp, format, i16, i32 def _load_properties(self): - # # font properties @@ -136,7 +133,6 @@ class PcfFontFile(FontFile.FontFile): return properties def _load_metrics(self): - # # font metrics @@ -147,7 +143,6 @@ class PcfFontFile(FontFile.FontFile): append = metrics.append if (format & 0xFF00) == 0x100: - # "compressed" metrics for i in range(i16(fp.read(2))): left = i8(fp.read(1)) - 128 @@ -160,7 +155,6 @@ class PcfFontFile(FontFile.FontFile): append((xsize, ysize, left, right, width, ascent, descent, 0)) else: - # "jumbo" metrics for i in range(i32(fp.read(4))): left = i16(fp.read(2)) @@ -176,7 +170,6 @@ class PcfFontFile(FontFile.FontFile): return metrics def _load_bitmaps(self, metrics): - # # bitmap data diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3202475dc..f42c2456b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -45,12 +45,10 @@ def _accept(prefix): class PcxImageFile(ImageFile.ImageFile): - format = "PCX" format_description = "Paintbrush" def _open(self): - # header s = self.fp.read(128) if not _accept(s): @@ -143,7 +141,6 @@ SAVE = { def _save(im, fp, filename): - try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 8d0a34dba..7eb82228a 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -35,12 +35,10 @@ def _accept(prefix): class PixarImageFile(ImageFile.ImageFile): - format = "PIXAR" format_description = "PIXAR raster image" def _open(self): - # assuming a 4-byte magic label s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index b6626bbc5..9078957dc 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -161,7 +161,6 @@ def _crc32(data, seed=0): class ChunkStream: def __init__(self, fp): - self.fp = fp self.queue = [] @@ -195,7 +194,6 @@ class ChunkStream: self.queue = self.fp = None def push(self, cid, pos, length): - self.queue.append((cid, pos, length)) def call(self, cid, pos, length): @@ -230,7 +228,6 @@ class ChunkStream: self.fp.read(4) def verify(self, endchunk=b"IEND"): - # Simple approach; just calculate checksum for all remaining # blocks. Must be called directly after open. @@ -397,7 +394,6 @@ class PngStream(ChunkStream): self._seq_num = self.rewind_state["seq_num"] def chunk_iCCP(self, pos, length): - # ICC profile s = ImageFile._safe_read(self.fp, length) # according to PNG spec, the iCCP chunk contains: @@ -425,7 +421,6 @@ class PngStream(ChunkStream): return s def chunk_IHDR(self, pos, length): - # image header s = ImageFile._safe_read(self.fp, length) if length < 13: @@ -446,7 +441,6 @@ class PngStream(ChunkStream): return s def chunk_IDAT(self, pos, length): - # image data if "bbox" in self.im_info: tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] @@ -459,12 +453,10 @@ class PngStream(ChunkStream): raise EOFError def chunk_IEND(self, pos, length): - # end of PNG image raise EOFError def chunk_PLTE(self, pos, length): - # palette s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -472,7 +464,6 @@ class PngStream(ChunkStream): return s def chunk_tRNS(self, pos, length): - # transparency s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -524,7 +515,6 @@ class PngStream(ChunkStream): return s def chunk_pHYs(self, pos, length): - # pixels per unit s = ImageFile._safe_read(self.fp, length) if length < 9: @@ -542,7 +532,6 @@ class PngStream(ChunkStream): return s def chunk_tEXt(self, pos, length): - # text s = ImageFile._safe_read(self.fp, length) try: @@ -562,7 +551,6 @@ class PngStream(ChunkStream): return s def chunk_zTXt(self, pos, length): - # compressed text s = ImageFile._safe_read(self.fp, length) try: @@ -597,7 +585,6 @@ class PngStream(ChunkStream): return s def chunk_iTXt(self, pos, length): - # international text r = s = ImageFile._safe_read(self.fp, length) try: @@ -721,12 +708,10 @@ def _accept(prefix): class PngImageFile(ImageFile.ImageFile): - format = "PNG" format_description = "Portable network graphics" def _open(self): - if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -740,7 +725,6 @@ class PngImageFile(ImageFile.ImageFile): self.png = PngStream(self.fp) while True: - # # get next chunk @@ -1264,7 +1248,6 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): mode = im.mode if mode == "P": - # # attempt to minimize storage requirements for palette images if "bits" in im.encoderinfo: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index dee2f1e15..5aa418044 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -51,7 +51,6 @@ def _accept(prefix): class PpmImageFile(ImageFile.ImageFile): - format = "PPM" format_description = "Pbmplus image" diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 7e8d12759..5a5d60d56 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -51,13 +51,11 @@ def _accept(prefix): class PsdImageFile(ImageFile.ImageFile): - format = "PSD" format_description = "Adobe Photoshop" _close_exclusive_fp_after_loading = False def _open(self): - read = self.fp.read # @@ -177,7 +175,6 @@ def _layerinfo(fp, ct_bytes): raise SyntaxError(msg) for _ in range(abs(ct)): - # bounding box y0 = i32(read(4)) x0 = i32(read(4)) @@ -250,7 +247,6 @@ def _layerinfo(fp, ct_bytes): def _maketile(file, mode, bbox, channels): - tile = None read = file.read diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index d533c55e5..3662ffd15 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -49,12 +49,10 @@ MODES = { ## # Image plugin for SGI images. class SgiImageFile(ImageFile.ImageFile): - format = "SGI" format_description = "SGI Image File Format" def _open(self): - # HEAD headlen = 512 s = self.fp.read(headlen) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 1192c2d73..eac27e679 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -91,7 +91,6 @@ def isSpiderImage(filename): class SpiderImageFile(ImageFile.ImageFile): - format = "SPIDER" format_description = "Spider 2D image" _close_exclusive_fp_after_loading = False @@ -200,6 +199,7 @@ class SpiderImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Image series + # given a list of filenames, return a list of images def loadImageSeries(filelist=None): """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" @@ -289,7 +289,6 @@ Image.register_open(SpiderImageFile.format, SpiderImageFile) Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") sys.exit() diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index c64de4444..6712583d7 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -30,12 +30,10 @@ def _accept(prefix): class SunImageFile(ImageFile.ImageFile): - format = "SUN" format_description = "Sun Raster File" def _open(self): - # The Sun Raster file header is 32 bytes in length # and has the following format: diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 20e8a083f..32928f6af 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -32,7 +32,6 @@ class TarIO(ContainerIO.ContainerIO): self.fh = open(tarfile, "rb") while True: - s = self.fh.read(512) if len(s) != 512: msg = "unexpected end of tar file" diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 53fe6ef5c..67dfc3d3c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -46,12 +46,10 @@ MODES = { class TgaImageFile(ImageFile.ImageFile): - format = "TGA" format_description = "Targa" def _open(self): - # process header s = self.fp.read(18) @@ -174,7 +172,6 @@ SAVE = { def _save(im, fp, filename): - try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index baa9abad8..2cf5b173f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -793,7 +793,6 @@ class ImageFileDirectory_v2(MutableMapping): return ret def load(self, fp): - self.reset() self._offset = fp.tell() @@ -938,7 +937,6 @@ class ImageFileDirectory_v2(MutableMapping): return result def save(self, fp): - if fp.tell() == 0: # skip TIFF header on subsequent pages # tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._prefix + self._pack("HL", 42, 8)) @@ -1059,7 +1057,6 @@ ImageFileDirectory = ImageFileDirectory_v1 class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False @@ -1582,7 +1579,6 @@ SAVE_INFO = { def _save(im, fp, filename): - try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 0dc695a88..e4f47aa04 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -28,7 +28,6 @@ from ._binary import i32le as i32 class WalImageFile(ImageFile.ImageFile): - format = "WAL" format_description = "Quake2 Texture" diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1d074f78c..d060dd4b8 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -35,7 +35,6 @@ def _accept(prefix): class WebPImageFile(ImageFile.ImageFile): - format = "WEBP" format_description = "WebP image" __loaded = 0 diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 639730b8e..0ecab56a8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -75,7 +75,6 @@ def _accept(prefix): class WmfStubImageFile(ImageFile.StubImageFile): - format = "WMF" format_description = "Windows Metafile" @@ -86,7 +85,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): s = self.fp.read(80) if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": - # placeable windows metafile # get units per inch diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index f0e05e867..aa4a01f4e 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -41,12 +41,10 @@ def _accept(prefix): class XVThumbImageFile(ImageFile.ImageFile): - format = "XVThumb" format_description = "XV thumbnail image" def _open(self): - # check magic if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index ad18e0031..3c12564c9 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class XbmImageFile(ImageFile.ImageFile): - format = "XBM" format_description = "X11 Bitmap" def _open(self): - m = xbm_head.match(self.fp.read(512)) if not m: @@ -69,7 +67,6 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 5fae4cd68..5d5bdc3ed 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class XpmImageFile(ImageFile.ImageFile): - format = "XPM" format_description = "X11 Pixel Map" def _open(self): - if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) @@ -68,7 +66,6 @@ class XpmImageFile(ImageFile.ImageFile): palette = [b"\0\0\0"] * 256 for _ in range(pal): - s = self.fp.readline() if s[-2:] == b"\r\n": s = s[:-2] @@ -79,9 +76,7 @@ class XpmImageFile(ImageFile.ImageFile): s = s[2:-2].split() for i in range(0, len(s), 2): - if s[i] == b"c": - # process colour key rgb = s[i + 1] if rgb == b"None": @@ -99,7 +94,6 @@ class XpmImageFile(ImageFile.ImageFile): break else: - # missing colour key msg = "cannot read this XPM file" raise ValueError(msg) @@ -110,7 +104,6 @@ class XpmImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] def load_read(self, bytes): - # # load all image data in one chunk From e79460e775a69c2cd9896144c3b340c4f98e3ad5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 09:46:01 +1100 Subject: [PATCH 124/220] Removed wget dependency --- .github/workflows/test-mingw.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ef8214649..737da7b94 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -59,8 +59,7 @@ jobs: ${{ matrix.package }}-python3-numpy \ ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ - ${{ matrix.package }}-wget + ${{ matrix.package }}-python3-setuptools if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ From e7d2750997cfbf88a6c93cbe1e598c8054d1676a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 10:01:10 +1100 Subject: [PATCH 125/220] Updated test images origin --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 790404535..1dd6c9175 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ docs/_build/ # JetBrains .idea -# Extra test images installed from pillow-depends/test_images +# Extra test images installed from python-pillow/test-images Tests/images/README.md Tests/images/crash_1.tif Tests/images/crash_2.tif From ed1d6633a1db0bf7e894bd372dcccd647cc0ee85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 10:53:59 +1100 Subject: [PATCH 126/220] Use checkout action for test-images repository --- .github/workflows/test-windows.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cf160a997..e938e999d 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -38,6 +38,12 @@ jobs: repository: python-pillow/pillow-depends path: winbuild\depends + - name: Checkout extra test images + uses: actions/checkout@v3 + with: + repository: python-pillow/test-images + path: Tests\test-images + # sets env: pythonLocation - name: Set up Python uses: actions/setup-python@v4 @@ -63,9 +69,7 @@ jobs: echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH # Install extra test images - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-test-images.zip -oc:\ - xcopy /S /Y c:\test-images-main\* Tests\images\ + xcopy /S /Y Tests\test-images\* Tests\images # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` From 165675314605d7363605918698df091b405ba283 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 7 Feb 2023 22:48:33 -0800 Subject: [PATCH 127/220] Add docstrings for getixif() and Exif --- src/PIL/Image.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 81123d070..d47c57334 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1432,6 +1432,11 @@ class Image: return {get_name(root.tag): get_value(root)} def getexif(self): + """ + Gets EXIF data of the image. + + :returns: an :py:class:`~PIL.Image.Exif` object. + """ if self._exif is None: self._exif = Exif() self._exif._loaded = False @@ -3601,6 +3606,20 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): + """ + Exif class provides read and write access to EXIF image data. + + Only basic information is available on the root level, in Exif object + itself. In order to access the rest, obtain their respective IFDs using + :py:meth:`~PIL.Image.Exif.get_ifd` method and one of + :py:class:`~PIL.ExifTags.IFD` members (most notably `Exif` and + `GPSInfo`). + + Both root Exif and child IFD objects support dict interface and can be + indexed by int values that are available as enum members of + :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and + :py:class:`~PIL.ExifTags.Interop`. + """ endian = None bigtiff = False From ed3cd75630352b26ee3b90a05c3d9b7fc0de041f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Feb 2023 10:11:54 +0200 Subject: [PATCH 128/220] Use 'rmdir' instead of 'rm -r' Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/install_extra_test_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 941bfbe84..1ef6f4e97 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -9,4 +9,4 @@ mv $archive/* ../Tests/images/ # Cleanup old tarball and empty directory rm $archive.tar.gz -rm -r $archive +rmdir $archive From f679e410bd3ee7be8e4c6dce0df0ca4cb4d1fb47 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Feb 2023 10:12:14 +0200 Subject: [PATCH 129/220] Use 'rmdir' instead of 'rm -r' --- depends/download-and-extract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index d9608e782..a318bfafd 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -8,5 +8,5 @@ if [ ! -f $archive.tar.gz ]; then wget -O $archive.tar.gz $url fi -rm -r $archive +rmdir $archive tar -xvzf $archive.tar.gz From c0a811e11678c3a6d8869fdc3739c7635b83ddd4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Feb 2023 08:36:41 +1100 Subject: [PATCH 130/220] Updated libjpeg-turbo to 2.1.5.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 89903c621..8a4494103 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.5/libjpeg-turbo-2.1.5.tar.gz/download", - "filename": "libjpeg-turbo-2.1.5.tar.gz", - "dir": "libjpeg-turbo-2.1.5", + + "/libjpeg-turbo/files/2.1.5.1/libjpeg-turbo-2.1.5.1.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.1.tar.gz", + "dir": "libjpeg-turbo-2.1.5.1", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From 0eac4f1942b278f618761c91ccec3f4422b90300 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 8 Feb 2023 20:34:45 -0800 Subject: [PATCH 131/220] Fix syntax --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d47c57334..f5fa4ec0d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3612,8 +3612,8 @@ class Exif(MutableMapping): Only basic information is available on the root level, in Exif object itself. In order to access the rest, obtain their respective IFDs using :py:meth:`~PIL.Image.Exif.get_ifd` method and one of - :py:class:`~PIL.ExifTags.IFD` members (most notably `Exif` and - `GPSInfo`). + :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and + ``GPSInfo``). Both root Exif and child IFD objects support dict interface and can be indexed by int values that are available as enum members of From 074c6afdc7d6c6990b9738c68c0fa3951d4f0855 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 04:40:57 +0000 Subject: [PATCH 132/220] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/Image.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f5fa4ec0d..813c237ad 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1434,7 +1434,7 @@ class Image: def getexif(self): """ Gets EXIF data of the image. - + :returns: an :py:class:`~PIL.Image.Exif` object. """ if self._exif is None: @@ -3608,18 +3608,19 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): """ Exif class provides read and write access to EXIF image data. - + Only basic information is available on the root level, in Exif object itself. In order to access the rest, obtain their respective IFDs using :py:meth:`~PIL.Image.Exif.get_ifd` method and one of :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and ``GPSInfo``). - + Both root Exif and child IFD objects support dict interface and can be indexed by int values that are available as enum members of :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and :py:class:`~PIL.ExifTags.Interop`. """ + endian = None bigtiff = False From a45211b811f1a0e4313b672c1629bedca857745a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Feb 2023 20:33:08 +1100 Subject: [PATCH 133/220] Updated freetype to 2.13 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 8a4494103..ff95581fa 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -253,9 +253,9 @@ deps = { "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 - "filename": "freetype-2.12.1.tar.gz", - "dir": "freetype-2.12.1", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501 + "filename": "freetype-2.13.0.tar.gz", + "dir": "freetype-2.13.0", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { From 5059e5c143ee4b9e625163897a5610611a325bef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Feb 2023 08:11:50 +1100 Subject: [PATCH 134/220] Do not raise an error if os.environ does not contain PATH --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 8f7f223f8..0c8b390de 100755 --- a/setup.py +++ b/setup.py @@ -243,6 +243,8 @@ def _find_include_dir(self, dirname, include): def _cmd_exists(cmd): + if "PATH" not in os.environ: + return return any( os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep) From 997932bc93c778919fe22f4838ef0929482fe520 Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Thu, 9 Feb 2023 23:23:29 +0100 Subject: [PATCH 135/220] Update HPND wording in LICENSE file --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 125bdcc44..cf65e86d7 100644 --- a/LICENSE +++ b/LICENSE @@ -13,8 +13,8 @@ By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: -Permission to use, copy, modify, and distribute this software and its -associated documentation for any purpose and without fee is hereby granted, +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be From a8e03e4dabd0fcb55fec922825dec9861e7ef103 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Feb 2023 20:11:50 +1100 Subject: [PATCH 136/220] Added Exif code examples --- src/PIL/Image.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 813c237ad..a280935a5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1433,7 +1433,7 @@ class Image: def getexif(self): """ - Gets EXIF data of the image. + Gets EXIF data from the image. :returns: an :py:class:`~PIL.Image.Exif` object. """ @@ -3607,18 +3607,36 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): """ - Exif class provides read and write access to EXIF image data. + This class provides read and write access to EXIF image data:: - Only basic information is available on the root level, in Exif object - itself. In order to access the rest, obtain their respective IFDs using - :py:meth:`~PIL.Image.Exif.get_ifd` method and one of - :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and - ``GPSInfo``). + from PIL import Image + im = Image.open("exif.png") + exif = im.getexif() # Returns an instance of this class - Both root Exif and child IFD objects support dict interface and can be - indexed by int values that are available as enum members of - :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and - :py:class:`~PIL.ExifTags.Interop`. + Information can be read and written, iterated over or deleted:: + + print(exif[274]) # 1 + exif[274] = 2 + for k, v in exif.items(): + print("Tag", k, "Value", v) # Tag 274 Value 2 + del exif[274] + + To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` + returns a dictionary:: + + from PIL import ExifTags + im = Image.open("exif_gps.jpg") + exif = im.getexif() + gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) + print(gps_ifd) + + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. + + :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: + + print(exif[ExifTags.Base.Software]) # PIL + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # '1999:99:99 99:99:99' """ endian = None From bb524018d35ea6450e56c3cd3db489eeb0f5a79c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Feb 2023 16:20:27 +1100 Subject: [PATCH 137/220] Raise an error when EXIF data is too long --- Tests/test_file_jpeg.py | 5 ++++- src/PIL/JpegImagePlugin.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index e3c5abcbd..b84661330 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -270,7 +270,10 @@ class TestFileJpeg: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() - im.save(f, "JPEG", quality=90, exif=b"1" * 65532) + im.save(f, "JPEG", quality=90, exif=b"1" * 65533) + + with pytest.raises(ValueError): + im.save(f, "JPEG", quality=90, exif=b"1" * 65534) def test_exif_typeerror(self): with Image.open("Tests/images/exif_typeerror.jpg") as im: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d7ddbe0d9..71ae84c04 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -730,10 +730,10 @@ def _save(im, fp, filename): extra = info.get("extra", b"") + MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 - MAX_BYTES_IN_MARKER = 65533 MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN markers = [] while icc_profile: @@ -764,6 +764,9 @@ def _save(im, fp, filename): exif = info.get("exif", b"") if isinstance(exif, Image.Exif): exif = exif.tobytes() + if len(exif) > MAX_BYTES_IN_MARKER: + msg = "EXIF data is too long" + raise ValueError(msg) # get keyword arguments im.encoderconfig = ( From 20daa1d049b8b0e321c282af073e05b114fb216e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Feb 2023 20:49:09 +1100 Subject: [PATCH 138/220] Fixed typo --- src/PIL/TiffTags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9b5277138..ac048ba56 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -312,7 +312,7 @@ TAGS = { 34910: "HylaFAX FaxRecvTime", 36864: "ExifVersion", 36867: "DateTimeOriginal", - 36868: "DateTImeDigitized", + 36868: "DateTimeDigitized", 37121: "ComponentsConfiguration", 37122: "CompressedBitsPerPixel", 37724: "ImageSourceData", From ac6b9632b4c90ff820c23420e699c68092166d63 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Feb 2023 11:11:39 +0200 Subject: [PATCH 139/220] Test Python 3.12-dev on macOS and Ubuntu --- .ci/install.sh | 3 ++- .github/workflows/macos-install.sh | 3 ++- .github/workflows/test.yml | 1 + docs/installation.rst | 12 ++++++------ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 518b66acc..6aa122cc5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -37,7 +37,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy + # TODO Remove condition when NumPy supports 3.12 + if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index dfd7d0553..1fc6262f4 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -13,7 +13,8 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install numpy +# TODO Remove condition when NumPy supports 3.12 +if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..8e06de4cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: python-version: [ "pypy3.9", "pypy3.8", + "3.12-dev", "3.11", "3.10", "3.9", diff --git a/docs/installation.rst b/docs/installation.rst index ea8722c56..93260e141 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -442,15 +442,15 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.10 | arm64v8, ppc64le, | | | | s390x, x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | From 826c98156ceaf619493af22808189500f9b0ae46 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 11 Feb 2023 16:36:13 +0200 Subject: [PATCH 140/220] Remove unused listwindows functions for Windows/3.12 support --- src/_imaging.c | 3 --- src/display.c | 73 -------------------------------------------------- 2 files changed, 76 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 05e1370f6..cece2e93a 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3984,8 +3984,6 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); extern PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args); -extern PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args); @@ -4069,7 +4067,6 @@ static PyMethodDef functions[] = { {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, - {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS}, {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, #endif #ifdef HAVE_XCB diff --git a/src/display.c b/src/display.c index 0ce10e249..a50fc3e24 100644 --- a/src/display.c +++ b/src/display.c @@ -421,79 +421,6 @@ error: return NULL; } -static BOOL CALLBACK -list_windows_callback(HWND hwnd, LPARAM lParam) { - PyObject *window_list = (PyObject *)lParam; - PyObject *item; - PyObject *title; - RECT inner, outer; - int title_size; - int status; - - /* get window title */ - title_size = GetWindowTextLength(hwnd); - if (title_size > 0) { - title = PyUnicode_FromStringAndSize(NULL, title_size); - if (title) { - GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size + 1); - } - } else { - title = PyUnicode_FromString(""); - } - if (!title) { - return 0; - } - - /* get bounding boxes */ - GetClientRect(hwnd, &inner); - GetWindowRect(hwnd, &outer); - - item = Py_BuildValue( - F_HANDLE "N(iiii)(iiii)", - hwnd, - title, - inner.left, - inner.top, - inner.right, - inner.bottom, - outer.left, - outer.top, - outer.right, - outer.bottom); - if (!item) { - return 0; - } - - status = PyList_Append(window_list, item); - - Py_DECREF(item); - - if (status < 0) { - return 0; - } - - return 1; -} - -PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args) { - PyObject *window_list; - - window_list = PyList_New(0); - if (!window_list) { - return NULL; - } - - EnumWindows(list_windows_callback, (LPARAM)window_list); - - if (PyErr_Occurred()) { - Py_DECREF(window_list); - return NULL; - } - - return window_list; -} - /* -------------------------------------------------------------------- */ /* Windows clipboard grabber */ From ab2809a44c2211741ce2eca132d9cc60619ea2b5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Feb 2023 11:11:39 +0200 Subject: [PATCH 141/220] Test Python 3.12-dev on macOS and Ubuntu --- .github/workflows/test-windows.yml | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e938e999d..306e34ca9 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows diff --git a/docs/installation.rst b/docs/installation.rst index 93260e141..1bfc65f4b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -456,7 +456,7 @@ These platforms are built and tested for every change. | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | -| | PyPy3 | | +| | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ From f6040bc8792e355a0b62ba961093cde8127d39d4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 11 Feb 2023 23:14:47 +0200 Subject: [PATCH 142/220] Docker tests: enable gcov support for codecov/codecov-action --- .github/workflows/test-docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 7331cf8ee..7d2b20d65 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -87,6 +87,7 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} + gcov: true success: permissions: From 0836c747f08fdfcdfd9be5ee858d3ad10bb5430f Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 11 Feb 2023 23:16:13 +0000 Subject: [PATCH 143/220] add gcov coverage to test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..87cad1f36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,9 +107,9 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 with: - file: ./coverage.xml flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} + gcov: true success: permissions: From 42683781d6d25913cf9274dd7cd14153e542cfd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Feb 2023 13:41:35 +1100 Subject: [PATCH 144/220] Updated CI targets --- docs/installation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1bfc65f4b..1b5719a8e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -447,11 +447,13 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.10 | arm64v8, ppc64le, | -| | | s390x, x86-64 | +| | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ From d9085541bf9ef82be182f209931e5326629df055 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Feb 2023 15:18:53 +1100 Subject: [PATCH 145/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e35a55965..626860e71 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise an error if EXIF data is too long when saving JPEG #6939 + [radarhere] + - Handle more than one directory returned by pkg-config #6896 [sebastic, radarhere] From da38395396c2f6322073a0f24a38b5780e46e89f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Feb 2023 21:56:23 +1100 Subject: [PATCH 146/220] Removed quotes from result in docstring --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a280935a5..63bad83a1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3636,7 +3636,7 @@ class Exif(MutableMapping): :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: print(exif[ExifTags.Base.Software]) # PIL - print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # '1999:99:99 99:99:99' + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 """ endian = None From bbbb8e6e21108b13a3ae978b4aa0b783aa10bddf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Feb 2023 22:05:51 +1100 Subject: [PATCH 147/220] Updated harfbuzz to 7.0.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index ff95581fa..bef8afa9d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", - "filename": "harfbuzz-6.0.0.zip", - "dir": "harfbuzz-6.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", + "filename": "harfbuzz-7.0.0.zip", + "dir": "harfbuzz-7.0.0", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From ad0e9dbaaf41e5c7abddf08d850cf0c0d98a3a92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Feb 2023 10:52:32 +1100 Subject: [PATCH 148/220] Fixed writing int as UNDEFINED tag --- Tests/test_file_tiff_metadata.py | 16 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 18 insertions(+) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index a4481d85f..fdabae3a3 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -216,6 +216,22 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" +def test_writing_other_types_to_undefined(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[33723] + assert tag.type == TiffTags.UNDEFINED + + info[33723] = 1 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[33723] == b"1" + + def test_undefined_zero(tmp_path): # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2cf5b173f..aaaf8fcb9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -764,6 +764,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(7) def write_undefined(self, value): + if isinstance(value, int): + value = str(value).encode("ascii", "replace") return value @_register_loader(10, 8) From b7630bd675bd260dfac5eb53bc23afe1ab8e09d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Feb 2023 11:41:32 +1100 Subject: [PATCH 149/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 626860e71..aa03cfc40 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Fixed writing int as UNDEFINED tag #6950 + [radarhere] + - Raise an error if EXIF data is too long when saving JPEG #6939 [radarhere] From 06ba226e7b6b9fbcbf74839bf123b4095d6f9c92 Mon Sep 17 00:00:00 2001 From: James Zern Date: Tue, 14 Feb 2023 17:28:16 -0800 Subject: [PATCH 150/220] image-file-formats.rst: correct WebP quality range 0-100, not 1-100 --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..56937b5c7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1126,7 +1126,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If present and true, instructs the WebP writer to use lossless compression. **quality** - Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest + Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest size and 100 the largest. For lossless, this parameter is the amount of effort put into the compression: 0 is the fastest, but gives larger files compared to the slowest, but best, 100. From 8935dad32e31a121906a92bfc3eacdc3e410a711 Mon Sep 17 00:00:00 2001 From: James Zern Date: Tue, 14 Feb 2023 17:29:06 -0800 Subject: [PATCH 151/220] image-file-formats.rst: document WebP 'xmp' option --- docs/handbook/image-file-formats.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..a85ca59dd 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1147,6 +1147,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: The exif data to include in the saved file. Only supported if the system WebP library was built with webpmux support. +**xmp** + The XMP data to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + Saving sequences ~~~~~~~~~~~~~~~~ From 0f2a4c1ae5d8492280af126dc7fda9eeef9b9d50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Feb 2023 19:19:17 +1100 Subject: [PATCH 152/220] Added "corners" argument to rounded_rectangle() --- ...agedraw_rounded_rectangle_corners_nnnn.png | Bin 0 -> 544 bytes ...agedraw_rounded_rectangle_corners_nnny.png | Bin 0 -> 685 bytes ...agedraw_rounded_rectangle_corners_nnyn.png | Bin 0 -> 649 bytes ...agedraw_rounded_rectangle_corners_nnyy.png | Bin 0 -> 755 bytes ...agedraw_rounded_rectangle_corners_nynn.png | Bin 0 -> 643 bytes ...agedraw_rounded_rectangle_corners_nyny.png | Bin 0 -> 775 bytes ...agedraw_rounded_rectangle_corners_nyyn.png | Bin 0 -> 741 bytes ...agedraw_rounded_rectangle_corners_nyyy.png | Bin 0 -> 844 bytes ...agedraw_rounded_rectangle_corners_ynnn.png | Bin 0 -> 656 bytes ...agedraw_rounded_rectangle_corners_ynny.png | Bin 0 -> 785 bytes ...agedraw_rounded_rectangle_corners_ynyn.png | Bin 0 -> 752 bytes ...agedraw_rounded_rectangle_corners_ynyy.png | Bin 0 -> 856 bytes ...agedraw_rounded_rectangle_corners_yynn.png | Bin 0 -> 737 bytes ...agedraw_rounded_rectangle_corners_yyny.png | Bin 0 -> 870 bytes ...agedraw_rounded_rectangle_corners_yyyn.png | Bin 0 -> 835 bytes ...agedraw_rounded_rectangle_corners_yyyy.png | Bin 0 -> 934 bytes Tests/test_imagedraw.py | 30 +++++ docs/reference/ImageDraw.rst | 2 + src/PIL/ImageDraw.py | 106 ++++++++++++------ 19 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nynn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yynn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png new file mode 100644 index 0000000000000000000000000000000000000000..3e79e21aedb620a6d057f927610e1f1a791fd5da GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV0`ZB;uumf=j|0kzC#W?3d&6b&3+Jg_Su>JzjK?ym2`v- zDJkj*<%_Sao1Z3b)A4uC?n|AnP7x|CqXb1^;MZ!(KL6Y1T^!TqfCBT;qYZc3<~;7% zpmv|-{P9qaM|(CrdocO-gvmhJbv&mR9xY67H$BamTX;lO{hAK@mZM_tBUJQwvZpcg z>4wGDC7msDh^{HW8ftO6@$ZRl=Ii^C&z_pG?Z=nq>E9zIFN?|7eOp}+b}Xj*P1)|h zy!(>-eD}%EeY#Pnzb7*K%f?S;{CDaOsJGfS?rtxAWoWbf=A4}`z-oFP+I6(Ttoqx>dPD}f)_xZ95B<$(x=d#Wz Gp$P!^IRgOz literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png new file mode 100644 index 0000000000000000000000000000000000000000..d825ad2631717dc05270f8c211bae1793ce1c256 GIT binary patch literal 649 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU<&YbaSW-L^Y-S&pv49Z4i~R{ zJpB3juLnDJYq741xqiwrUsdwNVOgdU9-tN&_%TyY`tfILv4eYOg}ck`%NLx}xxs^@ zyJdri%{`OfnP#gW_{BWCH}hSDN{g%0C_!NuyeXZTUw?k7=XywB9@TsQ{h;olN5RjI z?Er;(uK8a6eF9>fi5nkYJN`}lP|{haQ2BlxL9y`KU8@vzlny1`49^R9j?np)6q?*~ z-1*wU^&9{2?Wic2YAseT8+uhhtX?MK>IPAxFuTS$>wV1|&SgKI{Oi{XvBbIO*6q*u zb|!MaS1seaGd9mmAKNWxb|y(>j|^&kMy5PUPrr z+2Fy^edK;dSP5 z4k>N4((yl>994Z^yk%>VQoBK(%y$iyjXG;SKApV$;l(w-sy|GY?LKmBLr&E3jc?=h z`CcdO+SZrwOfSC9IQ?v2sbOIB{@c%1slAVI32QUmSD$$K*V=bC*DcevzIL=E_pe=f zn5pmO%*UsSPuHx8nJsRz-}g*i<<)EDYj#e}iG829e$}$ward5IyL|2Ut&OWPe=q)U z?T}n|{*&WxA1r@A{mh+zms3w`ze&AYu_wm9s`y9j{nX4Xyte+pcH?+&cbiHAvXg)z4*}Q$iB}g)$Z3 literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e95d487498b4dec519e1b2e3745d5d1bda62f2 GIT binary patch literal 643 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVDj>GaSW-L^Y-RJze5fp4uJ-B zKmJ(e`LN_>a_{Y^IJe1oW@6!IjfVw59Wc=Fyd)+}(xUT@FYN7BUz>5O#4|#r<*4tmL%TLaq`ubP1CnXk z5RrX3-#KxjfEec}K~WfVe)rwozxb!f9vNX^u!6%DmLh<3MVS20FPfX2{qNa=L_J;o KT-G@yGywp+4$-0j literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png new file mode 100644 index 0000000000000000000000000000000000000000..274d27984dcb7633223d2557d9d3cb44f109f99f GIT binary patch literal 775 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU^?gN;uumf=k3jdMT;Cn90KR+ zzqc##~$aFue*C+9jjj@ z{aLwZm9B6%|FOh!qm4fjWXhJ;3coJ8bFgOW=NknVr)B5O-}IUxaqpz5hw|R9yH}I? z|JS=iRi53gx@+?v$aY^&)#>TEc{I1>sQvM6syhFyH|Qo7rvBUbqcXL3!;Fs=(T9@m zHRp16w`_>`a^ixJ*!DYjr)`KRyuUxX#nmZd&1}B4hUFJc6DJCYl}l>>K9n?h@A?mi zloWM@4kfLQ-Ot+!RHOn@IZD97;MyUn@cHMGzbVZy+UUX2edOH+8{V5q&JoK0h4#qx zI|` v(89Ixy0atZ_db_BI=N^4Kcr-NAp1ABZ;XS5VFODSD5ZM3`njxgN@xNAqD46+ literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png new file mode 100644 index 0000000000000000000000000000000000000000..c5f40bfdbdd968e3392c9ec9e4103595519afe01 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU|Qqp;uumf=j~0$pxXuv4uO|n z9xN^1)gZ>y_f+Lyz@+bG6Ky7m*QcmQiUIY&zz=&{-)G0e@36#Y)f`njRF$(h>PPri z1N%cOZWSecJZF)ksxJ1v=Wy7rH%F@M@{;$RzI>zb;=Jr_9iOS3Tv{NX`0>?^Z7oMNa8$bSDxl1NnTV^80#9uEDEwmQN7u@%rJLU)^cvO>ghMBK_fTPx7>n zpUzo7-Z0gM%X6MLI6Xx~^rzQ+=Y^#>ApPbGyT~5PQvq{CEI^{3u6{1-oD!M5dtaSW-L^Y*59(JcoNhd@j1 z_j9eARYVP#YS-Hroa6D_v#cvxH_=}hs09XU{PVe=A5X90u)F%B$F=3_EjQC2;ifa> z6GL)uZrowuH#_C{qeB+Cm$u2i{=293r&fOQQP=Esm3vMtV{Q5BBgR!-Q&#xn(9hQ| z4^?@UbMG#%VLcz=^ZxOg|Trg8C01Z?}J@_v?6{t5bxES^LwxAG5aAz1Kc; z%VOem1F>_vMJGjwU)xay6wfmIEBEJcpKDTjgxT8b8{hAalf8DfBzJGq%>19Qyf<9R7W$>ejEFervLB)oaH@-N^mOzH_(i9qWB*&;D&+ z7af;)|JtFqPe0x~`mp`p-lxtQZ)Qh)-LdJ}tmCU?WD7FqSoW+pJSSQFD*oQ_nTLNc zJ>KHE{9VL!H`5uPdq2CUCH^(M^-;!j)xpAu>BpYy9#ww4+8&m$f%E~d{fw-szSh0M QadSYzp00i_>zopr09Uk5F8}}l literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png new file mode 100644 index 0000000000000000000000000000000000000000..efd27be4f9df7c8c7733920dbe1330f938a4c2f5 GIT binary patch literal 656 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU<&tiaSW-L^Y+HUzFQ6=42~V& z4&R@z-mWq6+Q%l|Jf@thXZt6NNuH)Se~<8C=xUyF(VRD4)!-kr*A>ui+Pbn{=n{Jo?+Vzb?~ z%jV_r5u1%Bi!9%5cP~A4nO5Cy(>0MZS9a~sv*Jp(oFKyeyL?R~U&zt@Wo@RX8&^e? zzOaA);rP4_X_gyeKTbWHc(!PPmfnt!8X=<7e;!lXsJrH8N$T!LTGnEnyotX5g!at8 zYdx)ag04)fN{j2GsEE%mPg$ODE7K7=q@*}XP!t9qH)emWoVESI+Z!7_IJ#RlcyMsT f5(fiAL;6?lyA?{unZGam3ljBo^>bP0l+XkKcMjwy literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png new file mode 100644 index 0000000000000000000000000000000000000000..d3acd01abe04c4577f0b930dda7611f10912112c GIT binary patch literal 785 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV7lSy;uumf=k3jddAA%y90FJT zdiehQZPzO+EZBH{Olq04`SHz98+ObpJn;~y2M*#df2{OA`+3?U&u4F@-k-nKV7)GP z_A2(zx^HB!z4>FaZ)JS7UCy@}iTk(wtgprFe|o;H`p2Wb&0o*i9@@2I#=N>^?T2T5 z-8ipq$MHk6rp{bqVe{paowjl4$7lC8q@^y?x_7r!Tg)=9z5wk!>5EJ(>KrYxSE56|)x8pVJ?F zy|X9TlKtkPKo6$~m36JA>htF`=cF80Qq&PTG)f?a!SzG4Ci@?c+{-x)7&ZVun{U$k#q-Iko=zcS`+7x6cm|Cbe>IvE#L^%Ke literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png new file mode 100644 index 0000000000000000000000000000000000000000..55ddbc033fbeea4f13492b24372bd501975c994e GIT binary patch literal 752 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVA}5K;uumf=j~0$pv4Xh4uNw% zJ*fRWvvsN81P0F^7iWAgGYL8TDBN7}G7C@-4A@^j{Ws6_UAgwVlsUGaf5ngo}jmc@sq*v}tt{#E=W#o^opr*NOcHF(=hi0s*D9(*YPV@<_*^#9!^O@1pu=r<}=`-e4k5+RZmio*X zy79#Y{r4ZexoEl0O?*YWQY6yr?X`0Z{~-R&N_w0yHDfj-n?#ImXMfW;>I`Ao&p)&d26y} zExYbKuQ@*Xr}=|N545z!=J%}v%EtIlAR*N!ef5 z&Ch3j-F?OU!NH#7WgkDCt3Q@#e3Q%bn`)x%_CQ~qHP4Or{bqzEJ|JE8i~Wb8(fz*) Q5l29xp00i_>zopr07Mr|_y7O^ literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png new file mode 100644 index 0000000000000000000000000000000000000000..c000b26e9671cd748319fbab7e8921d6a98ef3ae GIT binary patch literal 856 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV3zlEaSW-L^Y+Hwyu}70Zh=O# z*zNQ0?Ob7%)xpg6=V|}ecLghCJ9THL%>wFyfd{6sa@#{}^EBm_ysP{4J(BzL&7{Q8 ztfYH~UTnzvP_t*Hy|Vi1iIYEGV|yF1weEEIaoasRyuaSFR9bWN=$8_|_QR{bZJ6ix zE+XQp#AK1n3#Wh2>RH*9UwwGhu^Fp=~sI?t3b; zqg%ov^T3lN;bO?wE9vT&d1;~-8h|<#1mCtAq6q#oO#|zgwmDKB6UuXZ15?v$g)EDZ6KVdjnSY zS!PYvtYxRqO^rTXlN&Qz{K`GGSEQ&`{hr~cvU_Ft|??`t^}=W_kE*LmC1t7Yas*e+!$ zc|I_CndGn2)xC3i)K?@L^WNQ9xBAMFp76)Zds@G)-E_G)WT_fo``2h-dY*P=^L|(& c2htA9>tEb*3t(Q@KN}?K>FVdQ&MBb@08oo#djJ3c literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png new file mode 100644 index 0000000000000000000000000000000000000000..7056b4fd9347bcce6ce3ddc9d0735efd12b5d0c4 GIT binary patch literal 737 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU|Qkn;uumf=j{#ezDEus42}|K z559h8ygKfJ*@Ji`9zkoLk6b5L8r_}F4b%bydzQa`y-jNU?JoCY+Gd@HrQTM2le+pq zXH6jQvx>(*Cc3wq^36N$-FsN=uVv09*U;zYe6vd~behi7|G7PZSHD1(TTD0dbH((% z%=2nFZTGHKZ?&!a{_;cY{C)fP&hvYcm-v01{>+_!7nhq&yW)BFxoLG_qpbbC=i5TA zR-V1T`(`(H4sUjzedye~jk&p1B7S9)uC&jr)bU$yv*q8BSvd<OMTnC*-R6jXT$lxi4I`@A$Urx8`Ddczh7^+(T08hSCOU69s?-BP_WwFnn0|kNK9j%}TLfU4=a(s-@L${y(L<-PHR0aqqKv#(y{8_F)g*yZ>hHvbkkzcGjO>FZ%rh)A7i{%d@KU z)_;1#@_k>+yt*~kt#|iTm48}Z_v_cEsw(Ah?%ihl(#{>P|8nZkq^jW4mAZWEJ{&py z`_iOURq?0K=M@&Tx*v{;@(+EVcYK?i6_{80-+gHmAnw+#}zDbA7H(5h`ZwPfb#KMH7Ahss6C(a}s(D3O+yQ zcbk$YRv75a7Tdm-FZ&$(mZQAUWosjI< z#yxIM#TVIZv140~#{2KtbN*LI%ynd{gTe~DWM4fqXlD9 literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png new file mode 100644 index 0000000000000000000000000000000000000000..7f1f003440063facdb2a8ae9bb6455d62b5f1b0b GIT binary patch literal 835 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVCM34aSW-L^Y-TLqFV+G4vxz& z^IxC8oqy&QUxR%O{Ja(4d2ddf@llfVgb5>16BHcy{^i?_rNGpZ6=G|?~y;~v@w?<{9xN)&gT%XRZ{mHR93szk>{_Lag zF7}q^by8XA-c{DOt-eez|FJ=R&BxLMWnXW-ySc@zz0`2=s&fyXZOgOa3N4k}Zks)S zqel*Rc1>04ZmZer{cFzL{W4Rw`$*dcw|{CCcF9kFy^}p{S>abbfA``EEgL-6w3^!O zE#jMZbKU#4G995qN{XWdN*HXblAa&mzjL-7M>jCIIJ$lJ&1=qXIqG}tRB7Tw0kPL* zwemIUz%WZXdawWO`JO{=uCm%MWtMrNFZA{Hn$kOmx$mrcylh_ev~WwS zgty)=UgoY9+4kq%s}Iie>-O!L=l3Kt@%*~)XYTyJsLt*EYRRoiQ~ffF50Ce5U9#%j z>c4MhiCe$nUA=YQs&jb>(YyN&-SnFD%b+*8urBrDH`(lM4Xf<@AJ06uUgws?wVYk6 z?5-Z%*7onYY+VlLYB~KI>+Eh%^j&Se=B7(%?7?^2%H&#C?dq#Ne=A={>DQrEyTiAw zUR_!9=<2SQ^JKd_`Zj3&Q?-~MRayVJ@2Sk5Woysh4fJT)&=JAC`dyqQ`}&;A_uI;J zlv-SYVgh1pg*rZu#DecshQo!%8SzkmVTE(;2wjg~sgADZUY zzLQPNopVC<%BkOdPLLSzJG}blm+ueEt;Gbb4{yqhJ-#pft}HMnx2;?7amw@XsBLc| zHh13om9u;GtUt$;Qj#OSRvcXRb!A^p_~m)B*=DiP^PeY#UN*T~w(d^Uv+T#W_ALBn zc5%;+jQ9C#u6b=+w^=&$YRQY^)!DYWbFUv*wd}9^!&`^?jtW=qlC{}=&$#%%`|oMt zH*QB+#!b5)D_i&Ca`e8IL-$;+pPvdzun)FNS@!G?Og_f*_k4A)%rW^DiIdyjMeGmF zGI)IT$Ha{r_Jx+&xQAvHSgg4Z&K>`Lal^6= x1 - x0 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) - if d == 0: - # If the corners have no curve, that is a rectangle + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle return self.rectangle(xy, fill, outline, width) r = d // 2 @@ -338,12 +346,17 @@ class ImageDraw: ) else: # Draw four separate corners - parts = ( - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ((x0, y0, x0 + d, y0 + d), 180, 270), - ) + parts = [] + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ): + if corners[i]: + parts.append(part) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -358,25 +371,50 @@ class ImageDraw: else: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) - self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: - self.draw.draw_rectangle( - (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 - ) + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) if not full_y: - self.draw.draw_rectangle( - (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 - ) + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): """Draw text.""" From 60208a325083579361eaecc6d388e265aa52bea6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Feb 2023 10:32:55 +1100 Subject: [PATCH 153/220] Only allow "corners" to be used as a keyword argument --- src/PIL/ImageDraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index dc77c06ea..a55ebbe8e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -296,7 +296,7 @@ class ImageDraw: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, corners=None + self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None ): """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): From a55c2b42b9fdeb395cdc387ea768eb12e9a547bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Feb 2023 20:34:52 +1100 Subject: [PATCH 154/220] If following colon, replace Python code-blocks with double colons --- docs/deprecations.rst | 16 +++-------- docs/handbook/image-file-formats.rst | 4 +-- .../writing-your-own-image-plugin.rst | 12 ++------ docs/reference/Image.rst | 28 +++++-------------- docs/reference/ImagePath.rst | 4 +-- docs/reference/ImageWin.rst | 4 +-- docs/reference/open_files.rst | 4 +-- docs/releasenotes/6.1.0.rst | 12 ++------ docs/releasenotes/7.0.0.rst | 12 ++------ docs/releasenotes/9.0.0.rst | 4 +-- docs/releasenotes/9.2.0.rst | 8 ++---- winbuild/build.rst | 4 +-- 12 files changed, 28 insertions(+), 84 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a..0db19a64e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -177,9 +177,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -194,9 +192,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont @@ -336,16 +332,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..d6b42589a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1393,9 +1393,7 @@ WMF, EMF Pillow can identify WMF and EMF files. On Windows, it can read WMF and EMF files. By default, it will load the image -at 72 dpi. To load it at another resolution: - -.. code-block:: python +at 72 dpi. To load it at another resolution:: from PIL import Image diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 59dfac588..75604e17a 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -108,9 +108,7 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -Once the plugin has been imported, it can be used: - -.. code-block:: python +Once the plugin has been imported, it can be used:: from PIL import Image import SpamImagePlugin @@ -169,9 +167,7 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax: - -.. code-block:: python +:py:func:`PIL.Image.frombytes` function, use the following syntax:: image = Image.frombytes( mode, size, data, "raw", @@ -281,9 +277,7 @@ decoder that can be used to read various packed formats into a floating point image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax: - -.. code-block:: python +the following syntax:: image = Image.frombytes( mode, size, data, "bit", diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ad0abbbd9..976b148fc 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -127,9 +127,7 @@ methods. Unless otherwise stated, all methods return a new instance of the .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to -ITU-R 709, using the D65 luminant) to the CIE XYZ color space: - -.. code-block:: python +ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, @@ -140,9 +138,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop -This crops the input image with the provided coordinates: - -.. code-block:: python +This crops the input image with the provided coordinates:: from PIL import Image @@ -162,9 +158,7 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter -This blurs the input image using a filter from the ``ImageFilter`` module: - -.. code-block:: python +This blurs the input image using a filter from the ``ImageFilter`` module:: from PIL import Image, ImageFilter @@ -176,9 +170,7 @@ This blurs the input image using a filter from the ``ImageFilter`` module: .. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands -This helps to get the bands of the input image: - -.. code-block:: python +This helps to get the bands of the input image:: from PIL import Image @@ -187,9 +179,7 @@ This helps to get the bands of the input image: .. automethod:: PIL.Image.Image.getbbox -This helps to get the bounding box coordinates of the input image: - -.. code-block:: python +This helps to get the bounding box coordinates of the input image:: from PIL import Image @@ -217,9 +207,7 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize -This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: - -.. code-block:: python +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: from PIL import Image @@ -231,9 +219,7 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` .. automethod:: PIL.Image.Image.rotate -This rotates the input image by ``theta`` degrees counter clockwise: - -.. code-block:: python +This rotates the input image by ``theta`` degrees counter clockwise:: from PIL import Image diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index b9bdfc507..7c1a3ad70 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -60,9 +60,7 @@ vector data. Path objects can be passed to the methods on the .. py:method:: PIL.ImagePath.Path.transform(matrix) Transforms the path in place, using an affine transform. The matrix is a - 6-tuple (a, b, c, d, e, f), and each point is mapped as follows: - - .. code-block:: python + 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: xOut = xIn * a + yIn * b + c yOut = xIn * d + yIn * e + f diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2ee3cadb7..4151be4a7 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -9,9 +9,7 @@ Windows. ImageWin can be used with PythonWin and other user interface toolkits that provide access to Windows device contexts or window handles. For example, -Tkinter makes the window handle available via the winfo_id method: - -.. code-block:: python +Tkinter makes the window handle available via the winfo_id method:: from PIL import ImageWin diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 6bfd50588..f31941c9a 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -61,9 +61,7 @@ Image Lifecycle * ``Image.Image.close()`` Closes the file and destroys the core image object. The Pillow context manager will also close the file, but will not destroy - the core image object. e.g.: - -.. code-block:: python + the core image object. e.g.:: with Image.open("test.jpg") as img: img.load() diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index eb4304843..76e13b061 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -13,16 +13,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been dep Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: - -.. code-block:: python +Deprecated:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") @@ -79,9 +75,7 @@ Image quality for JPEG compressed TIFF The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG -encoder. The default is 75. For example: - -.. code-block:: python +encoder. The default is 75. For example:: im.save("out.tif", compression="jpeg", quality=85) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 80002b0ce..f2e235289 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -118,9 +118,7 @@ Loading WMF images at a given DPI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ On Windows, Pillow can read WMF files, with a default DPI of 72. An image can -now also be loaded at another resolution: - -.. code-block:: python +now also be loaded at another resolution:: from PIL import Image with Image.open("drawing.wmf") as im: @@ -136,16 +134,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index a19da361a..616cf4aa3 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -155,9 +155,7 @@ altered slightly with this change. Added support for pickling TrueType fonts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TrueType fonts may now be pickled and unpickled. For example: - -.. code-block:: python +TrueType fonts may now be pickled and unpickled. For example:: import pickle from PIL import ImageFont diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6dbfa2702..3dfb25840 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -59,9 +59,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -76,9 +74,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont diff --git a/winbuild/build.rst b/winbuild/build.rst index 716669771..d4275a274 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -96,9 +96,7 @@ directory. Example ------- -The following is a simplified version of the script used on AppVeyor: - -.. code-block:: +The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild From 43682de4bdb6c5f93340107386f86becbfbad25a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Feb 2023 08:12:57 +1100 Subject: [PATCH 155/220] Updated harfbuzz to 7.0.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bef8afa9d..35980f19c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", - "filename": "harfbuzz-7.0.0.zip", - "dir": "harfbuzz-7.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.1.zip", + "filename": "harfbuzz-7.0.1.zip", + "dir": "harfbuzz-7.0.1", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From 36bcc0a89866bf9ca3f79c61470d9ab4a58d74ec Mon Sep 17 00:00:00 2001 From: Jasper van der Neut Date: Tue, 21 Feb 2023 10:34:41 +0100 Subject: [PATCH 156/220] Support saving PDF with different X and Y resolution. Add a `dpi` parameter to the PDF save function, which accepts a tuple with X and Y dpi. This is useful for converting tiffg3 (fax) images to pdf, which have split dpi like (204,391), (204,196) or (204,98). --- Tests/test_file_pdf.py | 42 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 5 ++++ src/PIL/PdfImagePlugin.py | 19 ++++++++----- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 216b93ca9..a45b22bf6 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,6 +80,48 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) +def test_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + +def test_resolution_and_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=200, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..081b84963 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1497,6 +1497,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum image, will determine the physical dimensions of the page that will be saved in the PDF. +**dpi** + A tuple of (x_resolution, y_resolution), with inches as the resolution + unit. If both the ``resolution`` parameter and the ``dpi`` parameter are + present, ``resolution`` will be ignored. + **title** The document’s title. If not appending to an existing PDF file, this will default to the filename. diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index baad4939f..d4f1ef93a 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,7 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - resolution = im.encoderinfo.get("resolution", 72.0) + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) + + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] info = { "title": None @@ -214,8 +219,8 @@ def _save(im, fp, filename, save_all=False): stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, Filter=filter, BitsPerComponent=bits, Decode=decode, @@ -235,8 +240,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -245,8 +250,8 @@ def _save(im, fp, filename, save_all=False): # page contents page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) From be489287d2d8923ad57695f1bdf0d28db8be5757 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Feb 2023 18:59:51 +1100 Subject: [PATCH 157/220] Parametrized test --- Tests/test_file_pdf.py | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index a45b22bf6..2afec9960 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,32 +80,18 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) -def test_dpi(tmp_path): +@pytest.mark.parametrize( + "params", + ( + {"dpi": (75, 150)}, + {"dpi": (75, 150), "resolution": 200}, + ), +) +def test_dpi(params, tmp_path): im = hopper() outfile = str(tmp_path / "temp.pdf") - im.save(outfile, dpi=(75, 150)) - - with open(outfile, "rb") as fp: - contents = fp.read() - - size = tuple( - float(d) - for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") - ) - assert size == (122.88, 61.44) - - size = tuple( - float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert size == (122.88, 61.44) - - -def test_resolution_and_dpi(tmp_path): - im = hopper() - - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, resolution=200, dpi=(75, 150)) + im.save(outfile, **params) with open(outfile, "rb") as fp: contents = fp.read() From 0d667f5e0b756844e22dbb78b71c84279992ca2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 21:12:11 +1100 Subject: [PATCH 158/220] Do not read "resolution" parameter if it will not be used --- src/PIL/PdfImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index d4f1ef93a..4fa1998ba 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,12 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) - dpi = im.encoderinfo.get("dpi") if dpi: x_resolution = dpi[0] y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { "title": None From 21d13e1dea032d734572a9bb954c827aba2b29d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 23:22:18 +1100 Subject: [PATCH 159/220] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 34609314c..f36ad5d46 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.3) + assert_image_similar(result, expected.convert("RGB"), 0.5) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 5c8a9165abcf7c2d2ec2829681e88726dafddaf4 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 23 Feb 2023 15:18:11 +0200 Subject: [PATCH 160/220] 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 161/220] Fix up pytest.warns lambda: uses --- Tests/test_core_resources.py | 21 +++++++++++---------- Tests/test_decompression_bomb.py | 10 +++++----- Tests/test_file_apng.py | 17 ++++------------- Tests/test_file_dcx.py | 5 ++--- Tests/test_file_fli.py | 5 ++--- Tests/test_file_gif.py | 8 ++++---- Tests/test_file_ico.py | 4 +--- Tests/test_file_im.py | 5 ++--- Tests/test_file_mpo.py | 5 ++--- Tests/test_file_psd.py | 5 ++--- Tests/test_file_spider.py | 5 ++--- Tests/test_file_tar.py | 4 +--- Tests/test_file_tga.py | 4 +++- Tests/test_file_tiff.py | 8 ++++---- Tests/test_file_tiff_metadata.py | 6 ++++-- Tests/test_file_webp.py | 4 +++- 16 files changed, 52 insertions(+), 64 deletions(-) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 385192a3c..e4c0001d1 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -177,13 +177,14 @@ class TestEnvVars: Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) assert Image.core.get_block_size() == 2 * 1024 * 1024 - def test_warnings(self): - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"} - ) + @pytest.mark.parametrize( + "vars", + [ + {"PILLOW_ALIGNMENT": "15"}, + {"PILLOW_BLOCK_SIZE": "1024"}, + {"PILLOW_BLOCKS_MAX": "wat"}, + ], + ) + def test_warnings(self, vars): + with pytest.warns(UserWarning): + Image._apply_env_variables(vars) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 63071b78c..4fd02449c 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -36,12 +36,10 @@ class TestDecompressionBomb: Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 - def open(): + with pytest.warns(Image.DecompressionBombWarning): with Image.open(TEST_FILE): pass - pytest.warns(Image.DecompressionBombWarning, open) - def test_exception(self): # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 @@ -87,7 +85,8 @@ class TestDecompressionCrop: # same decompression bomb warnings on them. with hopper() as src: box = (0, 0, src.width * 2, src.height * 2) - pytest.warns(Image.DecompressionBombWarning, src.crop, box) + with pytest.warns(Image.DecompressionBombWarning): + src.crop(box) def test_crop_decompression_checks(self): im = Image.new("RGB", (100, 100)) @@ -95,7 +94,8 @@ class TestDecompressionCrop: for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): assert im.crop(value).size == (9, 9) - pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99)) + with pytest.warns(Image.DecompressionBombWarning): + im.crop((-160, -160, 99, 99)) with pytest.raises(Image.DecompressionBombError): im.crop((-99909, -99990, 99999, 99999)) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 51637c786..9f850d0e9 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -263,12 +263,9 @@ def test_apng_chunk_errors(): with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() - assert not im.is_animated - - pytest.warns(UserWarning, open) with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: assert not im.is_animated @@ -287,21 +284,17 @@ def test_apng_chunk_errors(): def test_apng_syntax_errors(): - def open_frames_zero(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated with pytest.raises(OSError): im.load() - pytest.warns(UserWarning, open_frames_zero) - - def open_frames_zero_default(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open_frames_zero_default) - # we can handle this case gracefully exception = None with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: @@ -316,13 +309,11 @@ def test_apng_syntax_errors(): im.seek(im.n_frames - 1) im.load() - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open) - @pytest.mark.parametrize( "test_file", diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index ef378b24a..1adda7729 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -24,11 +24,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(TEST_FILE) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 70d4d76db..cb767a0d8 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -32,11 +32,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(static_test_file) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index bce72d192..8f11f0a1c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -32,11 +32,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(TEST_GIF) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): @@ -1087,7 +1086,8 @@ def test_rgb_transparency(tmp_path): im = Image.new("RGB", (1, 1)) im.info["transparency"] = b"" ims = [Image.new("RGB", (1, 1))] - pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) + with pytest.warns(UserWarning): + im.save(out, save_all=True, append_images=ims) with Image.open(out) as reloaded: assert "transparency" not in reloaded.info diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 9c1c3cf17..4e6dbe6ed 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -212,12 +212,10 @@ def test_save_append_images(tmp_path): def test_unexpected_size(): # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/hopper_unexpected.ico") as im: assert im.size == (16, 16) - pytest.warns(UserWarning, open) - def test_draw_reloaded(tmp_path): with Image.open(TEST_ICO_FILE) as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 425e690d6..bdc704ee1 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -28,11 +28,10 @@ def test_name_limit(tmp_path): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(TEST_IM) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index f0dedc2de..ea701f70d 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -38,11 +38,10 @@ def test_sanity(test_file): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(test_files[0]) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 036cb9d4b..ff78993fe 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -23,11 +23,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(test_file) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 011e208d8..122690e34 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -21,11 +21,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(TEST_FILE) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 799c243d6..b27fa25f3 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -29,11 +29,9 @@ def test_sanity(codec, test_path, format): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - pytest.warns(ResourceWarning, open) - def test_close(): with warnings.catch_warnings(): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bac00e855..1a5730f49 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -163,7 +163,9 @@ def test_save_id_section(tmp_path): # Save with custom id section greater than 255 characters id_section = b"Test content" * 25 - pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) + with pytest.warns(UserWarning): + im.save(out, id_section=id_section) + with Image.open(out) as test_im: assert test_im.info["id_section"] == id_section[:255] diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 70142747c..d0d9ed891 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -57,11 +57,10 @@ class TestFileTiff: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self): - def open(): + with pytest.warns(ResourceWarning): im = Image.open("Tests/images/multipage.tiff") im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(self): with warnings.catch_warnings(): @@ -231,7 +230,8 @@ class TestFileTiff: def test_bad_exif(self): with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. - pytest.warns(UserWarning, i._getexif) + with pytest.warns(UserWarning): + i._getexif() def test_save_rgba(self, tmp_path): im = hopper("RGBA") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index fdabae3a3..9a5681526 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -252,7 +252,8 @@ def test_empty_metadata(): head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. - pytest.warns(UserWarning, info.load, f) + with pytest.warns(UserWarning): + info.load(f) def test_iccprofile(tmp_path): @@ -422,7 +423,8 @@ def test_too_many_entries(): ifd.tagtype[277] = TiffTags.SHORT # Should not raise ValueError. - pytest.warns(UserWarning, lambda: ifd[277]) + with pytest.warns(UserWarning): + _ = ifd[277] def test_tag_group_data(): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f1bdc59cf..335201fe1 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -29,7 +29,9 @@ class TestUnsupportedWebp: WebPImagePlugin.SUPPORTED = False file_path = "Tests/images/hopper.webp" - pytest.warns(UserWarning, lambda: pytest.raises(OSError, Image.open, file_path)) + with pytest.warns(UserWarning): + with pytest.raises(OSError): + Image.open(file_path) if HAVE_WEBP: WebPImagePlugin.SUPPORTED = True From acc7e0b469ac973bf1ab79bf8369a8b13769d169 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:00:24 +1100 Subject: [PATCH 162/220] Highlight code example --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..9a3ddcab7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1104,7 +1104,7 @@ using the general tags available through tiffinfo. Either an integer or a float. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. For consistency with other image formats, the x and y resolutions of the dpi will be rounded to the nearest integer. From 742aff3718cebdad390ad808e3cae181ea749e27 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:17:10 +1100 Subject: [PATCH 163/220] Replace Python code-blocks with double colons --- docs/handbook/image-file-formats.rst | 4 +-- docs/handbook/text-anchors.rst | 2 +- docs/reference/Image.rst | 12 ++----- docs/reference/ImageDraw.rst | 18 ++++------ docs/reference/ImageEnhance.rst | 2 +- docs/reference/ImageFile.rst | 2 +- docs/reference/ImageFilter.rst | 2 +- docs/reference/ImageFont.rst | 2 +- docs/reference/ImageMath.rst | 2 +- docs/reference/ImageSequence.rst | 2 +- docs/reference/PixelAccess.rst | 8 ++--- docs/reference/PyAccess.rst | 8 ++--- docs/releasenotes/6.2.0.rst | 8 ++--- docs/releasenotes/7.1.0.rst | 4 +-- docs/releasenotes/8.2.0.rst | 4 +-- docs/releasenotes/8.4.0.rst | 4 +-- docs/releasenotes/9.1.0.rst | 8 ++--- src/PIL/ImageChops.py | 52 +++++++--------------------- src/PIL/ImageFont.py | 12 ++----- 19 files changed, 44 insertions(+), 112 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d6b42589a..4c2af3db8 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1402,9 +1402,7 @@ at 72 dpi. To load it at another resolution:: To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF -handler. - -.. code-block:: python +handler. :: from PIL import Image from PIL import WmfImagePlugin diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst index 0aecd3483..3a9572ab2 100644 --- a/docs/handbook/text-anchors.rst +++ b/docs/handbook/text-anchors.rst @@ -29,7 +29,7 @@ For example, in the following image, the text is ``ms`` (middle-baseline) aligne :alt: ms (middle-baseline) aligned text. :align: left -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 976b148fc..0eba1141a 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -17,9 +17,7 @@ Open, rotate, and display an image (using the default viewer) The following script loads an image, rotates it 45 degrees, and displays it using an external viewer (usually xv on Unix, and the Paint program on -Windows). - -.. code-block:: python +Windows). :: from PIL import Image with Image.open("hopper.jpg") as im: @@ -29,9 +27,7 @@ Create thumbnails ^^^^^^^^^^^^^^^^^ The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. - -.. code-block:: python +current directory preserving aspect ratios with 128x128 max resolution. :: from PIL import Image import glob, os @@ -242,9 +238,7 @@ This rotates the input image by ``theta`` degrees counter clockwise:: .. automethod:: PIL.Image.Image.transpose This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` -method. - -.. code-block:: python +method. :: from PIL import Image diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9aa26916a..e325a0280 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -16,7 +16,7 @@ For a more advanced drawing library for PIL, see the `aggdraw module`_. Example: Draw a gray cross over an image ---------------------------------------- -.. code-block:: python +:: import sys from PIL import Image, ImageDraw @@ -78,7 +78,7 @@ libraries, and may not available in all PIL builds. Example: Draw Partial Opacity Text ---------------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -105,7 +105,7 @@ Example: Draw Partial Opacity Text Example: Draw Multiline Text ---------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -597,18 +597,14 @@ Methods string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = draw.textlength("Hello", font) world = draw.textlength("World", font) hello_world = hello + world # not adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # may fail - use - - .. code-block:: python + use :: hello = draw.textlength("HelloW", font) - draw.textlength( "W", font @@ -617,9 +613,7 @@ Methods hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 29ceee314..b27228ec9 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -10,7 +10,7 @@ for image enhancement. Example: Vary the sharpness of an image --------------------------------------- -.. code-block:: python +:: from PIL import ImageEnhance diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 3cf59c610..047990f1c 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -15,7 +15,7 @@ and **xmllib** modules. Example: Parse an image ----------------------- -.. code-block:: python +:: from PIL import ImageFile diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index c85da4fb5..044aede62 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -11,7 +11,7 @@ filters, which can be be used with the :py:meth:`Image.filter() Example: Filter an image ------------------------ -.. code-block:: python +:: from PIL import ImageFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 516fa63a7..946bd3c4b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -21,7 +21,7 @@ the imToolkit package. Example ------- -.. code-block:: python +:: from PIL import ImageFont, ImageDraw diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 63f88fddd..118d988d6 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -11,7 +11,7 @@ an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- -.. code-block:: python +:: from PIL import Image, ImageMath diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f2e7d9edd..a27b2fb4e 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -10,7 +10,7 @@ iterate over the frames of an image sequence. Extracting frames from an animation ----------------------------------- -.. code-block:: python +:: from PIL import Image, ImageSequence diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index b234b7b4e..04d6f5dcd 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -18,9 +18,7 @@ Example ------- The following script loads an image, accesses one pixel from it, then -changes it. - -.. code-block:: python +changes it. :: from PIL import Image @@ -35,9 +33,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index f9eb9b524..ed58ca3a5 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -17,9 +17,7 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the Example ------- -The following script loads an image, accesses one pixel from it, then changes it. - -.. code-block:: python +The following script loads an image, accesses one pixel from it, then changes it. :: from PIL import Image @@ -34,9 +32,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 20a009cc1..0fb33de75 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -10,9 +10,7 @@ Text stroking ``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing operations. They allow text to be outlined, setting the width of the stroke and and the color respectively. If not provided, ``stroke_fill`` will default to -the ``fill`` parameter. - -.. code-block:: python +the ``fill`` parameter. :: from PIL import Image, ImageDraw, ImageFont @@ -28,9 +26,7 @@ the ``fill`` parameter. draw.multiline_text((10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0") -For example, - -.. code-block:: python +For example, :: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0024a537d..cb46f127c 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -10,9 +10,7 @@ Allow saving of zero quality JPEG images If no quality was specified when saving a JPEG, Pillow internally used a value of zero to indicate that the default quality should be used. However, this removed the ability to actually save a JPEG with zero quality. This has now -been resolved. - -.. code-block:: python +been resolved. :: from PIL import Image im = Image.open("hopper.jpg") diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index c902ccf71..f11953168 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -76,9 +76,7 @@ ImageDraw.rounded_rectangle Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as :py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` argument. ``radius`` is limited to half of the width or the height, so that users can -create a circle, but not any other ellipse. - -.. code-block:: python +create a circle, but not any other ellipse. :: from PIL import Image, ImageDraw im = Image.new("RGB", (200, 200)) diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index 9becf9146..e61471e72 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -24,9 +24,7 @@ Added "transparency" argument for loading EPS images This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", generating an RGBA image with a transparent background instead of an RGB image with a -white background. - -.. code-block:: python +white background. :: with Image.open("sample.eps") as im: im.load(transparency=True) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index e97b58a41..19690ca59 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -182,17 +182,13 @@ GifImagePlugin loading strategy Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as -well. - -.. code-block:: python +well. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS Or subsequent frames can be kept in ``P`` mode as long as there is only a single -palette. - -.. code-block:: python +palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index fec4694b2..701200317 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -38,9 +38,7 @@ def duplicate(image): def invert(image): """ - Invert an image (channel). - - .. code-block:: python + Invert an image (channel). :: out = MAX - image @@ -54,9 +52,7 @@ def invert(image): def lighter(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. - - .. code-block:: python + the lighter values. :: out = max(image1, image2) @@ -71,9 +67,7 @@ def lighter(image1, image2): def darker(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the darker values. - - .. code-block:: python + the darker values. :: out = min(image1, image2) @@ -88,9 +82,7 @@ def darker(image1, image2): def difference(image1, image2): """ Returns the absolute value of the pixel-by-pixel difference between the two - images. - - .. code-block:: python + images. :: out = abs(image1 - image2) @@ -107,9 +99,7 @@ def multiply(image1, image2): Superimposes two images on top of each other. If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. - - .. code-block:: python + you multiply with a solid white image, the image is unaffected. :: out = image1 * image2 / MAX @@ -123,9 +113,7 @@ def multiply(image1, image2): def screen(image1, image2): """ - Superimposes two inverted images on top of each other. - - .. code-block:: python + Superimposes two inverted images on top of each other. :: out = MAX - ((MAX - image1) * (MAX - image2) / MAX) @@ -176,9 +164,7 @@ def overlay(image1, image2): def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 + image2) / scale + offset) @@ -193,9 +179,7 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 - image2) / scale + offset) @@ -208,9 +192,7 @@ def subtract(image1, image2, scale=1.0, offset=0): def add_modulo(image1, image2): - """Add two images, without clipping the result. - - .. code-block:: python + """Add two images, without clipping the result. :: out = ((image1 + image2) % MAX) @@ -223,9 +205,7 @@ def add_modulo(image1, image2): def subtract_modulo(image1, image2): - """Subtract two images, without clipping the result. - - .. code-block:: python + """Subtract two images, without clipping the result. :: out = ((image1 - image2) % MAX) @@ -243,9 +223,7 @@ def logical_and(image1, image2): Both of the images must have mode "1". If you would like to perform a logical AND on an image with a mode other than "1", try :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. - - .. code-block:: python + as the second image. :: out = ((image1 and image2) % MAX) @@ -260,9 +238,7 @@ def logical_and(image1, image2): def logical_or(image1, image2): """Logical OR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((image1 or image2) % MAX) @@ -277,9 +253,7 @@ def logical_or(image1, image2): def logical_xor(image1, image2): """Logical XOR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((bool(image1) != bool(image2)) % MAX) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index bd13c391e..173b2926f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -297,27 +297,21 @@ class FreeTypeFont: string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = font.getlength("Hello") world = font.getlength("World") hello_world = hello + world # not adjusted for kerning assert hello_world == font.getlength("HelloWorld") # may fail - use - - .. code-block:: python + use :: hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning world = font.getlength("World") hello_world = hello + world # adjusted for kerning assert hello_world == font.getlength("HelloWorld") # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) From 19299acbb6688d2aaf33e53f64c9cbb04545e274 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:39:48 +1100 Subject: [PATCH 164/220] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index f36ad5d46..4929fa933 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.5) + assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 3c3d88845072b0b8b8a9a00787fb03d733def3b7 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Thu, 23 Feb 2023 23:19:13 +0100 Subject: [PATCH 165/220] Update docs/handbook/image-file-formats.rst Co-authored-by: Hugo van Kemenade --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 081b84963..597d1b644 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1498,7 +1498,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum saved in the PDF. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. If both the ``resolution`` parameter and the ``dpi`` parameter are present, ``resolution`` will be ignored. From 57acab55cbf0f00f3064d11ccfd3843c55cdceb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 12:55:13 +1100 Subject: [PATCH 166/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa03cfc40..d37ba4ab3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Support saving PDF with different X and Y resolutions #6961 + [jvanderneutstulen, radarhere, hugovk] + - Fixed writing int as UNDEFINED tag #6950 [radarhere] From 44c4e67fe13719672219aa2de67b8c19f921c191 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:26:18 +0200 Subject: [PATCH 167/220] Fix `vars` to `var` Co-authored-by: Hugo van Kemenade --- Tests/test_core_resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index e4c0001d1..cb6cde8eb 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -178,13 +178,13 @@ class TestEnvVars: assert Image.core.get_block_size() == 2 * 1024 * 1024 @pytest.mark.parametrize( - "vars", + "var", [ {"PILLOW_ALIGNMENT": "15"}, {"PILLOW_BLOCK_SIZE": "1024"}, {"PILLOW_BLOCKS_MAX": "wat"}, ], ) - def test_warnings(self, vars): + def test_warnings(self, var): with pytest.warns(UserWarning): - Image._apply_env_variables(vars) + Image._apply_env_variables(var) From f52bbf895036f25932a479a46b20b07f2eb0c1de Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:58:51 +0200 Subject: [PATCH 168/220] Clarify variable names in BdfFontFile Co-authored-by: Yay295 --- src/PIL/BdfFontFile.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index e0dd4dede..3f7b760d6 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,25 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - [x, y, l, d] = [int(p) for p in props["BBX"].split()] - [dx, dy] = [int(p) for p in props["DWIDTH"].split()] + # The word BBX followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBox, BBoy) of the lower left corner + # from the origin of the character. + width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) + # The word DWIDTH followed by the width in x and y of the character in device units. + dx, dy = [int(p) for p in props["DWIDTH"].split()] + + bbox = ( + (dx, dy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) try: - im = Image.frombytes("1", (x, y), bitmap, "hex", "1") + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") except ValueError: # deal with zero-width characters - im = Image.new("1", (x, y)) + im = Image.new("1", (width, height)) return id, int(props["ENCODING"]), bbox, im From b6b72170a8d81255e6f491e657e0614102a0e347 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:59:54 +0200 Subject: [PATCH 169/220] Clarify variable names in Image Co-authored-by: Yay295 --- src/PIL/Image.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 63bad83a1..670907c67 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -767,12 +767,12 @@ class Image: data = [] while True: - l, s, d = e.encode(bufsize) - data.append(d) - if s: + length, error_code, chunk = e.encode(bufsize) + data.append(chunk) + if error_code: break - if s < 0: - msg = f"encoder error {s} in tobytes" + if error_code < 0: + msg = f"encoder error {error_code} in tobytes" raise RuntimeError(msg) return b"".join(data) From 04be46d484eccb633b1ad8058e1c4f39208589b0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:04:38 +0200 Subject: [PATCH 170/220] Clarify variable names in ImageFile Co-authored-by: Yay295 --- src/PIL/ImageFile.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 132490a8e..dfa715686 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - l, s = encoder.encode_to_pyfd() + length, error_code = encoder.encode_to_pyfd() else: if exc: # compress to Python file-compatible object while True: - l, s, d = encoder.encode(bufsize) - fp.write(d) - if s: + length, error_code, chunk = encoder.encode(bufsize) + fp.write(chunk) + if error_code: break else: # slight speedup: compress to real file object - s = encoder.encode_to_file(fh, bufsize) - if s < 0: - msg = f"encoder error {s} when writing image file" + error_code = encoder.encode_to_file(fh, bufsize) + if error_code < 0: + msg = f"encoder error {error_code} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() From 6f79e653d68c7bae9c0303f8d8ddd68a3a1cb3f0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:07:29 +0200 Subject: [PATCH 171/220] Clarify variable names in PcfFontFile Co-authored-by: Yay295 --- src/PIL/PcfFontFile.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index d5f510f03..1a4b8f6d9 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,9 +86,23 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] - self.glyph[ch] = glyph + ix_metrics = metrics[ix] + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = ix_metrics + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) def _getformat(self, tag): format, size, offset = self.toc[tag] @@ -206,7 +220,7 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y, l, r, w, a, d, f = metrics[i] + x, y = metrics[i][0], metrics[i][1] b, e = offsets[i], offsets[i + 1] bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) From 8e18415cc54c1c87183d36a184b5360d77fb8571 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:09:14 +0200 Subject: [PATCH 172/220] Clarify variable names in TiffImagePlugin Co-authored-by: Yay295 --- src/PIL/TiffImagePlugin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index aaaf8fcb9..0491a736d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1845,13 +1845,13 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16 * 1024) + length, error_code, chunk = e.encode(16 * 1024) if not _fp: - fp.write(d) - if s: + fp.write(chunk) + if error_code: break - if s < 0: - msg = f"encoder error {s} when writing image file" + if error_code < 0: + msg = f"encoder error {error_code} when writing image file" raise OSError(msg) else: From 1263018d2afbc19e9dce8b82d064cccc2e5ccfca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 23:00:29 +1100 Subject: [PATCH 173/220] Assert value instead of assigning unused variable --- Tests/test_file_tiff_metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 9a5681526..b7d100e7a 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -419,12 +419,12 @@ def test_too_many_entries(): ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd._tagdata[277] = struct.pack(" Date: Sat, 25 Feb 2023 16:44:07 +1100 Subject: [PATCH 174/220] Allow comments in FITS images --- Tests/test_file_fits.py | 6 ++++++ src/PIL/FitsImagePlugin.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index d2f5a6d17..3048827e0 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -44,6 +44,12 @@ def test_naxis_zero(): pass +def test_comment(): + image_data = b"SIMPLE = T / comment string" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + def test_stub_deprecated(): class Handler: opened = False diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 1185ef2d3..1359aeb12 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -32,7 +32,7 @@ class FitsImageFile(ImageFile.ImageFile): keyword = header[:8].strip() if keyword == b"END": break - value = header[8:].strip() + value = header[8:].split(b"/")[0].strip() if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): From dbcd7372e554ee420a156b968eb9a609ec66ffd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 18:46:07 +1100 Subject: [PATCH 175/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d37ba4ab3..15decc7f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow comments in FITS images #6973 + [radarhere] + - Support saving PDF with different X and Y resolutions #6961 [jvanderneutstulen, radarhere, hugovk] From 132fb9360b291eafedd3ea8238ceebd93a094e87 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 19:10:47 +1100 Subject: [PATCH 176/220] Added memoryview support to frombytes() --- Tests/test_image_frombytes.py | 11 +++++++++-- src/decode.c | 7 +++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 7fb05cda7..c299e4544 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,10 +1,17 @@ +import pytest + from PIL import Image from .helper import assert_image_equal, hopper -def test_sanity(): +@pytest.mark.parametrize("data_type", ("bytes", "memoryview")) +def test_sanity(data_type): im1 = hopper() - im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) + + data = im1.tobytes() + if data_type == "memoryview": + data = memoryview(data) + im2 = Image.frombytes(im1.mode, im1.size, data) assert_image_equal(im1, im2) diff --git a/src/decode.c b/src/decode.c index 7a9b956c5..82a3af832 100644 --- a/src/decode.c +++ b/src/decode.c @@ -116,12 +116,11 @@ _dealloc(ImagingDecoderObject *decoder) { static PyObject * _decode(ImagingDecoderObject *decoder, PyObject *args) { - UINT8 *buffer; - Py_ssize_t bufsize; + Py_buffer buffer; int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { + if (!PyArg_ParseTuple(args, "y*", &buffer)) { return NULL; } @@ -129,7 +128,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionEnter(&cookie); } - status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize); + status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); if (!decoder->pulls_fd) { ImagingSectionLeave(&cookie); From 36489c2c396d8801bfa632b6e8d00a094dab5347 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 20:54:35 +1100 Subject: [PATCH 177/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 15decc7f0..fe0230c34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added memoryview support to frombytes() #6974 + [radarhere] + - Allow comments in FITS images #6973 [radarhere] From fcc59a4001bb1b1ad47d1a8c0b0a602336b2dcdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 13:40:44 +1100 Subject: [PATCH 178/220] Use existing variable names from ImageFile --- src/PIL/Image.py | 14 +++++++------- src/PIL/ImageFile.py | 14 +++++++------- src/PIL/PcfFontFile.py | 11 ++++++----- src/PIL/TiffImagePlugin.py | 10 +++++----- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 670907c67..cf9ab2df6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -765,17 +765,17 @@ class Image: bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - data = [] + output = [] while True: - length, error_code, chunk = e.encode(bufsize) - data.append(chunk) - if error_code: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: break - if error_code < 0: - msg = f"encoder error {error_code} in tobytes" + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" raise RuntimeError(msg) - return b"".join(data) + return b"".join(output) def tobitmap(self, name="image"): """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index dfa715686..8e4f7dfb2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - length, error_code = encoder.encode_to_pyfd() + errcode = encoder.encode_to_pyfd()[1] else: if exc: # compress to Python file-compatible object while True: - length, error_code, chunk = encoder.encode(bufsize) - fp.write(chunk) - if error_code: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: break else: # slight speedup: compress to real file object - error_code = encoder.encode_to_file(fh, bufsize) - if error_code < 0: - msg = f"encoder error {error_code} when writing image file" + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 1a4b8f6d9..2300efe40 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,7 +86,6 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - ix_metrics = metrics[ix] ( xsize, ysize, @@ -96,7 +95,7 @@ class PcfFontFile(FontFile.FontFile): ascent, descent, attributes, - ) = ix_metrics + ) = metrics[ix] self.glyph[ch] = ( (width, 0), (left, descent - ysize, xsize + left, descent), @@ -220,9 +219,11 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y = metrics[i][0], metrics[i][1] - b, e = offsets[i], offsets[i + 1] - bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) + left, right = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (left, right), data[b:e], "raw", mode, pad(left)) + ) return bitmaps diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0491a736d..04d246dd4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1845,13 +1845,13 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - length, error_code, chunk = e.encode(16 * 1024) + errcode, data = e.encode(16 * 1024)[1:] if not _fp: - fp.write(chunk) - if error_code: + fp.write(data) + if errcode: break - if error_code < 0: - msg = f"encoder error {error_code} when writing image file" + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) else: From c799bd8a03f522dc6cc26f6c974140df60558484 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 14:04:10 +1100 Subject: [PATCH 179/220] Adjusted variable names and comments to better match specification --- src/PIL/BdfFontFile.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index 3f7b760d6..075d46290 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,18 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - # The word BBX followed by the width in x (BBw), height in y (BBh), - # and x and y displacement (BBox, BBoy) of the lower left corner - # from the origin of the character. + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - # The word DWIDTH followed by the width in x and y of the character in device units. - dx, dy = [int(p) for p in props["DWIDTH"].split()] + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = [int(p) for p in props["DWIDTH"].split()] bbox = ( - (dx, dy), + (dwx, dwy), (x_disp, -y_disp - height, width + x_disp, -y_disp), (0, 0, width, height), ) From bbbaf3c615e7a60e526e73f3dc6449780dce2271 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sun, 26 Feb 2023 13:03:29 +0200 Subject: [PATCH 180/220] Update src/PIL/PcfFontFile.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/PcfFontFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 2300efe40..8db5822fe 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -219,10 +219,10 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - left, right = metrics[i][:2] + xsize, ysize = metrics[i][:2] b, e = offsets[i : i + 2] bitmaps.append( - Image.frombytes("1", (left, right), data[b:e], "raw", mode, pad(left)) + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) ) return bitmaps From 9c98f4d515036d9bb8094bc5faac6a81eca0b147 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Feb 2023 09:48:41 +1100 Subject: [PATCH 181/220] Release buffer --- src/decode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decode.c b/src/decode.c index 82a3af832..7e3fadc04 100644 --- a/src/decode.c +++ b/src/decode.c @@ -134,6 +134,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionLeave(&cookie); } + PyBuffer_Release(&buffer); return Py_BuildValue("ii", status, decoder->state.errcode); } From 17eadf07fa5315e6d904936fb052e90c49915962 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 28 Feb 2023 20:49:43 +1100 Subject: [PATCH 182/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fe0230c34..d5798d41b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added "corners" argument to ImageDraw rounded_rectangle() #6954 + [radarhere] + - Added memoryview support to frombytes() #6974 [radarhere] From 6e9c0ae5a09618afec077a39262d2337dd0a3fee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 28 Feb 2023 20:04:26 +1100 Subject: [PATCH 183/220] Further document that x1 >= x0 and y1 >= y0 --- docs/reference/ImageDraw.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 81e3d8f46..9df4a5dad 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -318,8 +318,8 @@ Methods Draws a rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param outline: Color to use for the outline. :param fill: Color to use for the fill. :param width: The line width, in pixels. @@ -331,8 +331,8 @@ Methods Draws a rounded rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. :param outline: Color to use for the outline. :param fill: Color to use for the fill. From 53fb3a9365feec24cb59196477639bf712849ef0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 11:04:14 +1100 Subject: [PATCH 184/220] Updated lcms2 to 2.15 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1b5719a8e..55d5ee832 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -150,7 +150,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.14**. + above uses liblcms2. Tested with **1.19** and **2.7-2.15**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 35980f19c..3a885afaf 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -289,9 +289,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", - "filename": "lcms2-2.14.tar.gz", - "dir": "lcms2-2.14", + "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", + "filename": "lcms2-2.15.tar.gz", + "dir": "lcms2-2.15", "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { From b84c29a035b2476ae1152fc2054107f25d562dfb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 11:22:35 +1100 Subject: [PATCH 185/220] Raise an error if co-ordinates are incorrectly ordered --- .../imagedraw_ellipse_various_sizes.png | Bin 21446 -> 21600 bytes ...imagedraw_ellipse_various_sizes_filled.png | Bin 20315 -> 20325 bytes Tests/test_imagedraw.py | 28 ++++++++++++++--- src/PIL/ImageDraw.py | 6 ++++ src/_imaging.c | 27 ++++++++++++++++ src/libImaging/Draw.c | 29 ++++++------------ 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png index 11a1be6faebea1a31854e4d13975f1a5c12c811e..5e3cf22b4ad8e19506b7c9551996f522bff75046 100644 GIT binary patch literal 21600 zcmbt+1yodf+pmBKf|QgXjX|n_AdRAgN=SD|jnX0AB?w5UAdQj&14A528WfNkgBe;n zq`QW=d(QykdC&R2_ujjf=d5+kau3Yj|L1vr_3Rgq6l919XbDc8Iz@C}_MY;oQ`ltS zKls?d|8d`Skvers_3Hh5cT`;x=aUAV8U^9GgVi;tTGK`U&aHLL^SG~W$7j~v(ctin zi2ZbB?{#MLW33odt=O9IF+upMU^om0YxZv*TkihVVt(U7L{@2WFxJ{|=^H+aYg-n~ z6CV@|`AQiTmBJ`hEPrR&V+?P=GgSH2b!bP-I5wP9< z4|Y^MJ`4jf_TiHh6-`6W-=qoqcJz2{g?xqk#@>%y^e&*Z}mHmUf zm~Ct3Zg}(XlY}OPxMrxg-0|=4aFf+!z<`il#t}QAaBqq7eau}v$#GkyXYtrCC+ z6Zo!F94U}=t2A;5RZC$lm2&vZ>3k!RS!MO+vt0O5jZ&Zg!hU2&!6jUTq%ZL9aeclB z{$+=@(;c|p-05_Pv$~(ny7K~6@DKm8nKi|`;Oxc+U(C1OEY`vcfP3?3yHB-GK-kXA zmkx!#|HB-m#n((76YKm)g>+TrI8S+6J$RtrNLB;hd+y7k@K|BvgXLGN=K@}*<|S%5 z)YlTQL%(ZTTD6?hE_F%MQEX z(>vERmOam9bV{t8djh;(TUH2j_2CyqKR3+Rlu9;8SaE$0=PL&vt&3dC4k zKP5s_6e|pU1APt?TurtMaHJaARCjkQplpIWS-#h4oW3K--iJP7xseSVNhPJ24*(z3 z=_%m3(qgys66$TEzhC$&H*nA_u64RLO`lB+S`jsVSSO^S(^|os)xz&Z3O=@`!X!ub zT-MW60WEhP3BL6Dnj=WC-@6*dRpHP(4;jHqS*pnXg|lQQT9QOhG^T>PNLsbxB`w#q zhd$u86I`%|T4GX$cY^6otF?qks+^LNqf5A)74D<8X~?!jG$IOL{DtywhW~qE4U3hQ zT3=0+1V7_ihywTt^y_ZJT6#zSG^IZp$bCxn48$i%a)O?_U@Qacg_ypnMl5pj6_cGSmU9~n66*zZILkg zr=1S`L6A4``8FAOv)PxVGpof*Tti@8Udc&>h?;36-?dF9YxWEH=HIWbkf_B31BzNO zr&QhHvmn?Cm`ER78SuCbJTe@+%;^?w=em9IDH<)WQ;Ocm4IcMR#)J9GAt;*WuOAm! zC0ry}4V#4U#`9{a&dXSJbDS11MvYKlW8XLZ4i+OVG1OG>W$ILK{ev%BE-|$)#Ney_ zcHT={Li@t>T102Ymb;t|o5IfyCZP#fplExxairZv7kNvG6Sq$03a272c=N8_-VB5O z@27BM2vVi$a;pJ&nw6^-TeYkGb&C!N%fIzH*4Hk{cbdWKeC@rfBT`dP3omC#tVyv$ z@&8|ZDaKEo?oC(}7U>F<8-@F3zE#S`2uzVMGTuOj7*IB=!>5VqO8G^MkF(?XJp%WvnKr{h`~bV#J6l>8Rs^)Qp%NsGBrpOx?tN9yPg?Bdy*z*7E@F(w;9W|~Z-?fkstRGwAnt&cdL{fkl}GwG6g-f zQ0plajV$fE_r=%Fg9BC&51thPj>{VX$q*uN$fD|2MQiCd@gh>jU_YV|!!2=FsPD-` z$Z)3Xt=J(*-KB2HRwZLY^UwCj#b8eR^SyjyLLFssN)|Tw84g|5Y6G16X*&@ehID^c z)E_sK9qfc7sS7u)^;EieSuhh@1nK<)qNRSRRp!b5>SOuu5POQl9woPVuC0iL4DK6z zG~KVRMt5hb@Bcb3*u#(KKa;7>)x6Po_yaOWI%p{tyYHDvUc)7<5{QgkE5)Vx_VRSLgH}SJ#U%j{AAwdeXiQ7R*oF<;7?A-3@8J+ExK*$P$b3Rva~mN z>PaOtfgduVGw{6~!~wI}DME0~?J}k2tVBq93;+Jjbh^OPF&cBQiveS3fMi5W9i3t2 z8{)sYDFnaW@?FI@MGU?bw@55xo%Bo7IW3|pB`T0&D}?>hBc6Ve#q0IjfzuV zAtt{Y0~*Qx=aar(bU-`+)PdwgGROo>$1PMyF3R2ok>fF3wk7bzWwtLrJb*n>?HyWp zDW?*|jCW3X{64xu_Ga~2k*z~(N&BdC z2cAuq5B~KQZV9A+=VK`5gp*kZ?_mdAP-g%z?aR)dM3(NYACdMH=QZ^UYpqGc$#;ZE z`Cb5MIHnZW?5WPXV68@$aat;X6VRhk@t%x0JXloF>RA&=BvPI;DV3DObLJbNMg-rd z;PAYTeJ%C*<_`02LL$Vn@FUI!i8y@AcQHX5I3ieAMeH9fLgX~Gh|In$r}mPXaA@X| zmM6Vb>FYdC)K3>rntCpzR;L|213-uZo?@QXLh2|^M;oa>+FlCD>nx#Qg;Euh8c&e5z=mNlv;l#!%nz#?0A-S5OXb^Df)kML{3y?o*8e127&x_)W*o-i4(FZnr z+f>tqDwI~I_+BE7I7mX#Oi+^DU9~)emyp8R#yS_Fu}G>J3B~(zRhq2$fzYqIcyjpb(CB!? zeqP04W2)&WX$=(3;KrH~wPc4_DnwcAKE*!|LikY*DiWLg&Q@k0699Y|f#3rft7Ey% zmz`=!W{(%XM49rV1OW2;A3Tzy3O<9xDq>&zU#?ZhX_EW)7W>f<%(z3gs9 zMjTsbK4|z0(Iqkf|B#2!Elm90NDBMO>_aMw*e`Kku<~&(Pr)*iH8Cp;8V=+zdDS6w z5jzgPy1OyRIN%Rk7I_9DK+Js$);Rq(U#J)yDMcZY$Q;}=0+$2|d`BoG&)qkb961z9 ztc@vq1-_WOPvA~AY*+5b|_P=4+DI_e?Y@L zBv>s(p1;MlGyC%H-$o_CEcQ(}+@zQ2)c5VoMM5v!Cb52qxe=f)5Wc_i@eP6ygND)S zkM3jk-jEjaEjr50Fdj36C@`1DT)+!QDeE&VFn0i&8d=h+Gu5g-9U5p$!2CR&_r2}m z_$(_7u}lEy52gNgO*YVPc^$;5w7IL}TY>JkjP6VmZy27ttP^7e3}^3D-j%)@%N!N= znZb`bC-T0)lw}K2_ZLk>Z}b$tLAlk7-p>4UI@q1AJw_mZl=0?1{UQfA33imBLANv@ z6Qs9`=+wn|LKJebjLnEH@HqlP7vW-L*^)S#;O}3ZcWvsTL6M|D481O;7d(LEJ!N75 z*~(5xo!{ZP1%(NOjJ26fQ3%5&@TfH1^f--aOtl2P`Peo1?;c~ssneo| zB=IjRid&7m7|y}A8(%KrvP|TTw0h)&Fw*6k6s`* zIN0?X6s=$Y!C<&bZtpNCMV;*NG)aK~s>uXU%@K9(Bj{<5>;y*mxX)eD6rF3X1}LTH zO*s^|NYIn_<qzXuyYd2rJqAKLgPuD>p0E-$7k;@5C% zGIpBK5;8u(dg~MY7|`EHbml(A;jT=}ND|~eL&l#OMRP6~K9_7vW9wDvZ%_FR+qs5v zl*TJQJGJPztma4GbN~t4PQ>qq`S(K`8Tg2IWRm=y`Cq3#2)cNet%k*NiBWDXuZAh2 z4rqWvxpTfeg0Toilk2GQ<&DPKbBi7lX%)goX#+t_`a}oWD4KzRdo-h7-}LZ)aR*`V z8rs}wc1<3tnXP9nMs4x)heOZ!Hv<}!K;{T5THMswk0lYOH*s=5r%Y_w)HE_YBM<5p zmV4}7NO(uyDKuZB33BF5Y1SvlF!!-8a{-s%s2PG3bt7=BQm=0-5mDo8JDQB@Y(_6t zx=MI&1kc~VP(FuvF=Op9b=2L0b<31(#yOfmVg20UiN3+?z7DrRSl1icjvUWVZq0di zAs;h8G^Fn2=2me5&F$S@JV0EiyBBJGY)(mw9%-^IVy#8`*E2j#rszxzs!jVkq;$cIP@MYH{d*Zt&-t4m zhuJzbNwD}f!=5Z!_xW*K6jbWhDVYi1n>2J>y^VN+Rp9YDEctQ!oT0J(s=)heQ`}W> zv}$zlWs2tPztn>kn$BY7NE7-1yku_m+yOizfZK_oCS?Rgv*;`_HeKyJGjRK$c?e$j z`pabEOTR=AQs(0|d#7x8?#9!C%yG#RCL_$Cwd4Oe- zAl{vfBUp)xBPHZKoKRN?36p5XgDukZyM?G;woPf?oUu&mK!zUCiLO>NPcGHOE01v> zBg7wwn}R3@=!2GW3sX@4>&)57JTYxg<F#cni0*=uZefUcVa!rn4+n#ko1Tt5R@)n_=^?2{N=2>SEoW-N4Zg)`aA#C*Cy`ZIu!Sf~9Cr>Nmn;iG=PQlzEZSnC4Q>V%m>L!n<~MTX+ou6idn#t} z=QqBvIC@Jt>z}n<6IFqm+rW*DS(n(WSq&4UxAx4A< zejX(CT6X3iC|`Qv9{|D|mO9`pbWYLmF!~?KjUXA*={_~G3z-0h6oAh^-*)5X1gMCWOHdV&O~cy& zKn$9URqe`O=1~FySL4cO;vF8b|G_L>A;U zjAF}t^t#*3{Fj=yftxj@6~W>LkF}8Msp45a5guQL*5s4MFqHB)(NPYk?I@u{&6*t< zC+HK3xflI?J>%Ek-urhM1aa1~DZf1l@C1(@L7n~cXPLy6L|L20EFSkSxHmQKxO9F@ zCKD+uM30lB)+cCNX#@$HTXcM*7h2*SV~`8wQ=2Ql`4v7>((6dLFFExUVDwB&5(mMf zZcAb=dUYQo3{*%OFYr4y?b{97Z6uF~aYo*tC$WsUDz0ZIOc|@8XoafY5cD?vAjRxE z0eqpRQVQml_h;|h5gVegga)9b67-se94nvFo{fu(65q2tMp@oXBTp z?@XI~dCh^ztZ20B5obIQLC|EcAc@$w;wz z*EdyvX8@^-$rxPY$h4b{PmfVcSr;e>1H&|znHOjxyhzJJHWz7zk>42ACw{q%}4(ReHe(+Pkn1h%L|7ojWfV+a5%05|V)4 z?V(X8-Djh;s>K+3^(Wc^Z}CoBWJ?tP@~+nh)V7s7$GR7MW7fIdxYbo3EA1iF^jWls zyqAVPKs>7IZt)@_TmLCMG&$m5m4p^+u6(D@`vJ6*|5Oc+aoCmAp>wqhH`%Oxhv1D^AmF?y7&j$yRhNd+}X^ko-x*i}q=))Y{Lc1~k|3 zPM%t?|2bQ~zAEmu=a%9r*bFpJG#g(I8UD_HrJ z+I1rf(2ri-04U44sTeC@Bet=Y6Oq8r6CC&4;*?G2bFKLZ*YI+p>zVf`o@B0d&!uWP zlwN!Rdf_!^=QhI-*eX+=m-4&pn579AHRvu}S)vEGE!Ifoox6fd?3g_4zCn8He~Locd%3Q%yGxN((H-WWMkM? z@$L$TF$DwYuq@mZeFzHWfPwd3{jZfgLThWRg2rRK!fv?v7~vhbuz(Ty*@IMUs5A3oZjTo&&Gl2hLZt zk3INta-KB;L$bkz`%-vFH;2jh6p-2CpKcFmz)E5wcs6>IF18gTlc-WitQJBwTT710N$(w1VK<8!X3K&yC zt5nExLslpFFvbNrik=wH^>3W}T~t&+QGM0lqYs?2t9;pdVfQo@I~5aNh@wfMgQ8SaI4qD zUWqaR4+)vXy=3jQkvdgym~vjuO5KXvbZ;_$nY}80USp=5ly%}fSGFOMJY|PvR4Xz{ z`xm=L+)C!qfYpw>UI^*<*C7d&$^|Fmwp2`Eez0!Gf1Pmk0^$;@5M;zqJ$Nd``<2_) zfiEoZYf*a;%`#+rLam8Sw4ln!d+@1C448xb5~6Z9@5uz*)Ak514okhUI+x`kD-&M< zz;@F;MR9mf50FT4-4gY$KC0z_SU6y@JNj^DP6yOY(eUpdAYyi{Z^&vT3d5od&4n)l zc`rLUOG`T#c3f_6-ob@Fr^xo!f|uH+*foPClsGzkbXURIX&>kB!S(@r{*+Syg?tqA z`O6J4Ut|sA&NZd(&Sp06Ei_kCi!FrcPTFPs8XjA2t$j%#*g@uDi%7M*Vpm0_jN=sB z(d#8Bc^hzcmP$(LzMe_7rA@v_4Hi^3sZuBZmiYK*G1d>LUaIpymM}aQh)$YJuy+?J zESn>nz2w^$ay@0+nUTcm>iif}2*2WWF)>46gqZQJkhn`@vhN&xnE1KP?OtJ2saV=k zGy<$)t&#|x#qvY>p%KHYI`6NC73wNj7!^qDP9#TRu;`J@CZGKxrH}9M3QbjS$LP0G z>Wz$lX?V+w2n{@_7KiZz=GEevwpQ~JrO$4b_WYQv*g$As- z+gfJ)3_=qN(S^ zt%=_{S)Zx!aR3F^yE(!yglg+9mLk3e!Z~tX^M7Enm?t%hP5GB&Dkn^B<<4(DuPr<- zfoaxbw4jAsR5bJTIOr<`nWG^Y0??6LBic!gJ`Ciww5n+4RyBNdV$C9b$KKAptqAlstG#$e8DWcA#}$2|a|%Q5p4bEZ&+D9oPH0E`#X zFIaTr2L@JO7SH&<`JBBz;-W`{z=<9H?m;k}^q zT&57Cn*1z)FiBwzXsGm0RIZ0<<6iGx0EZ<|+9rrQ>W){Crq)uKz_8?=1yyRH=PJA@ zx6%2CTn4=vx0c*zU^^50?h$>0)cKrWXwup+l>tH{ zp)E_DnS1Ck`LT~Q5BTeNUjUf|+I`<{{F<;?_y3A%G4))n@QLJ+-YxeE#@6$oxB~_* zTnt7QJ*^zrZA_B#iYrXfa1c1@FHk~dCSM67ryE6)v+EXaP3HYl0@k*8b(^od7@`OF zs+_fu39@$T_pauSkksTrp!?VCJb3NyJ7Vn_M#F8)kIf;lSB7g^wMCo-wW#uE?^J>FwM|FCgk`ejG%@JdCE~<(4tBjZ!f%Rn3WU%sB5l0ntZU42t;_ zsd^>bDl<|M3nQEPaYHy+{0l>V?#?6^uGpkuD>$5HltUz0MsZM-XwyR}p`A5-18|gS zYVA~zc*L$1+n;eBqrZu#^@ zo!e9M`d>YFX3f?Pav=Le8VvoHdQR4d-mxFMqE+kB52k^_ZdxZ;KTpS$Li3=<0&aTu0nUcsz z-W#2fZCu7A!)K8NvJWN_>owna$1N0&srifrM8yyOyL zwcKwL@jco89UPH;4)Fm!1Mq+ZMIa@~?t@<(lZ8!q2H9e0NW<_s95aGF;XPC`>1;kI z4|5&wvSg+HSFM0)%bBnqrA&S(?!@Ut{c@C7Y7>^D7q}8^pIL`ig$QSJeDrz^1i}6-_cIyN#W{S1*Bm%`!Q7KpN_JzuU~1xvrKq%>@CJuvBBQA#9*!N28``)a zi_Nu3QT?4Zm5#rxylV9^ec!1Y@8D$(q36yee={7~+wc*+&qZc48jRKte_#@pXzE%O zQ3@062_QHbvet0v-5xFin`|+PMER^U#;5rS#krzYA$~TG-DPV{v(nFmT}F> zEdcK+4<9B$W(s)jyy3&GxfCWg_P81leL$1Q0N}Bh*&hAA^fCe|3GUhe`3HebzLCk- z;Jyx119T$0&d+zvOU_rO@cIG`=Yasn@J*Sedey-%RN0bBoO035W1ag$Q2LI&IAtLtAK@iSfqMOG*J*=^6^Fvo;5$^Gfg!1 zai()vqV=>?_p=lEE`++&gnQ4nIKszKPR*{Bi*}5)(hSa0((qOq6f7HI3G|hP-6h@cJuCzy~rVo&2#%8y0L`;E`5ZY~t4 z(ZlI~;&4`6dq(h}_yT|1=?s>F4aHF};#7T`Hes@amblL~{@^@XJ0#ez=4pV*d^i|a zW2@79b2M`OOFn*OXX`NP;!;Kn)>J^DK+oU3&Sq+u$oy8T$r`6k^O8CNo|r*%t>gH| zt#!|JK>2I}L~GWFP~9}hMA%+85YGVgC}%}W0F|4?=#EK9E`8|J?x1Y$u|&^X71@`K zt&qUfAHu*^h3l+j6y$d1z>{wibah(jg@PTKMVDl57?Tkb zbEuQFQ-IN!pkxH)`ElvfF>%cc*@)PFkJet6LS+CvoSgK=J*!zDn7{uLSZyHE4HVZ; z=(D~Vn7wc5Fth+Ac4H$Q@E>oK94WMB2nDjBuwwaCBwyXm zPn%0x=(vFT-8RQ!6;2fY2S7)5j8sDfm-y(gPdjoGp1xZocO%^=`jwrF(5mTh4cS(nEhLFrtJ4QARtAv|e`uUJ+9K8d<`vos-+AQqB|97lJZ}dqvQC`=^?oq^@Aod3eAn*-lYdDw!uv?P4q3zyxI;gJXqu#x%TMSo=Y0aXS-q*JmQ z)>HeUQ&;#?I}z_EdAts$lkIik!l_po+Gg`JG?62Kud2?p!_W8`!SKzI;lGsC4jY99 zJ2J?ca!BaX4g)J0$YB;eHp4b4_kr4fyCRM36;9A`@@{@Nrw!eEU7q6b@cHy=JQ8o( z!A-g#*Qb)araNN;KG!)nh$w=FKZD3N16_H`Wc1ZX?(+Nsb*&DEB;u3S>Nwdo%J-0g zl--<9l||iegd^&YQ~Z}S-v#eMNe)*}7`IW$%hek_=fvGN@8&S%!`%XN zDHM0=W~n&%E5F@cjIs5$-Q67G8*GMU+nMlR5Dm{V;v2=2K}YMxDy*^yaxKw|y*)^+ zq-Y#su=54Pbi4!(mbvHKQP;~fL2vmC&eW7cpukG7&udy>?~}Hiz_Us~EuS}Or7N3$ zkM~Zmjh2Xng#?}jR<~2+&MaY~OELkpOLui_vbuJNPg-%fD&lm8lg>(HJU&vs;87-l zO3nl8GM{gca??vzzGaI59b;gY4G;mQAc#P&poD*_Fu`enCk%4KLJfvAV4eM@P_z)x z!Vd}oj55rH&3Rv$FkpeJqRBg87`q z${Zm2exhx7Zvq+e^d~^uaN|6>PBYGSH7-^7FZQofG~T14V;_ASYhO7|iRatuO|qNF zS@YMldQk>`3Xg+{IxBc!**#3?Wa%c-sNLA+B)Je+RUym7P}xcZghUu(2_x062m-%^ zK)M{EJu7xwJ5EmtVY`+1gk@9@@$RW)UV>VWO&|AhoM&jR@V5?AGg2A3Ah5aLGF zV@N?MJr&So;x-0F;s-t6&MmIV1_lNLHeEY+l0qwIx2R1rY97oQ>zCbL=CpmfqwI4e zsi}Y1)R_Om5VyGQiU+1}6t{l?tz#wDYq{(Cpoc-`d}XY;>;$;8ZW~teY*gLkP z9-14VO)(&4=c1UAPj3pNrOc;BLS2bQpVlM%cC$l>!;0{SR#2ewrG-!QI~F z8eY@Qiy^Ge-*_q)o+JT_Yd|A72S|b7a@PNjF`yDTos8yTL+@6L|6D5ux=BoA*;SN2 z)lXF4r1H-eVI7D=+rr|7R7IE$lJOfD*`xKPAS?Jfyx!v2T=et7+A8xkw3q^N3(tcu zKs$LbKeJoUkuucPaA*?(z{A!=`??B%O1$5D4Poy}H?=0$r{{Z)fd1d`1&d#ws-0x! zAf87SMa$4;*kXx=tV@9JHn{SS7{rd^y!!xwyhoCA+x>3~3ZKA*rwCF}^v*Spz{Q`s zK%?XYR)bYkg9LB|7NKAHba_xclyUYp_uo34f-IxA+!(h-ft8iNfK2eeHk0R$wXr}m zxn}>b%_KAbBFkGcmECB(Fv*{~z&?xK#F?fI4U%@;77p@600&)E&cv-S^<6s`Rd+?8 zQ~i#01MuC_mfD<2%4NiFzuo4sKj*$RjY*qX#D=5M$Wbr39XAhss71g0h^ZX!3m%jn z3RUi;h+L1SXlJoRdYI@JLWL6Ae{#*xw2W1RWGQgIQU`0$Zam`sez=z|kf?`4`^&uO@VyrsX2`cTZ@TR*uIN>K}ExdPZvs_1^4 zS&c37N)rZq!0!Gxu|tVtHas+Mh(-Ob@bmeRm-|!S*Y2O`ICGQkCmvTW^UiYRXp7#n z_WazcdqBu{xK;`7;#=oVvPGHRRgyNRmIgqR{dG@Z3*t5q%hFVdhLJhu6Z69e*J1cd z`M8Do*kph57Pr6l-O($6>&o9MzrD9~B>8VCj6TQbAnRru$MegBe9*OjKqhCZPqA~v ze|Rtx0-IJTxQ-l}p;?ypvEj_lV>CVM$bQV9L`RW&>pz((H8UNgUZVTj2;pUM4N}` z#XITePkb-#2!QI48{`V;qhBdvtmq0X8Z&`LKdCB5?&L+K8`N&xN!g8~d<8+W7Bz!#tqgMn)ehaa)j5jRIwjC= z@^}JhB$oLPwLC9%F@Z5qIVJi&is{v`O$oT>GWpUhEppEYNxxIx;ux0|$y1Ej;#gOs zI4@!*z7B3-C(7&Hx_6B=bL$wtpqJ+j74EPBPzc>k%eW$wna}rpYU<@^k>3&!#X5mo zBI>jGHedz0QF)Jr{cM$Q+D5Oq#2DyjMBx>~Hkr(^#xgFb=LBvQ~#PlW=> zMcy33;pm$f!cliiY4HaT+j-eF!T5_N+dMXpIxzLoPncIaeoJ#?&2)0}>bv#`&=`3$>fPMJ(1Bbx9>X}-m<~e?559%GU zVhdlHkBl2;9?44`xyKeGjS$lP!7(fHzV;Gsf-sBhZ4^mfXJ=t(bBGxI1t5<<<&l({ zOux5J`(2d5cLIqI;BvRQ8G9 zS(1!%V9@_9l;N69ySIIpXs5fCy(UXxZP586tn0OxR?1Gosp~)<2u0xi50TUR18}L7 z)-q_2IFP(^r!R+$irAWKz>&C%yD_HKdHREVO&@9Ac3IUs29}85eY2X3e$T+MiYf^& zpDN>6&Y!c%Z*VL)7ExK7-4xk{>7u|K_cQn-@7ymia+hjC9`J=nJgqYyhN@c+(N;U< z#^!_W`EbBz5wMjozHr<~fL9ao5HTLVxj{X-0T3DLk9Oef@Hum75BL4S!?G{_pFyFk zH#UF+HhI{0_&9U9zzggwnRUp7C_gWDq$cwKF+55q{)QP+7xuYZuNwZWg}c4mFQfu= zc+2xH6Q)xmyF%~U1ZY9(ug~azr~r)9HVQXsQjl9Hih=915S0=fD4C%5MD~1QQuW8; z&5c{nTJx3JI6HtM|HN#n9jQYl3nx8mX*6a@Iex|CB5^}4T3u}%M}-6G>N;9oy(N9z zZ+Jg}``mwPHXXowZ6X3_174lImBOBLHvv0gGx8TreyrzQh+IWzew)y+_xC+-Fl@v< zzIrYN@)fXDdUSz>HQ5j74uK|1OGI$KOwGaGx)ecpdH&t=)R0e$DKD*+9k&P$lG zffRG0@uUh#!~Jhfy?H_|fv7`(GYaHzH73OmH#UgbfA|d)-FAE4Z_?^&4ZLT{c+*5= z+&^}Eq7rX_mGNEM?ggRe*gU}dmIx|owXBE=vBsIs4#z!f0>e{*bPL5vo;6k1Vs&2o z+--%uVqqNkl40umrn}UvQ~mJ{p;}A&SD_YI@&^c0#Fzo0Sen3b*o0jM!TSr2j^3TN zJ3qUkSV9E^Uq2Oo7l>rZk|bD???SDGmD{p9Zi5EsE3;XJyc)o#dA2b=sel-AU~`Pr zLl{{`j5K)^}I6~$K$%M^I$JWEr;osG#vFtYP6=eeEvZLI`lH#VJC z>ZI6u6S*JWpt3vk&MT>NiB-RMag*DFv0x}TJW^vT|5t8O;Ni|@er&B$NB2nGz76SH z#jR^=?q<0*)qAmR{%e;->g(wFK98Z7RJt zV{S+?F;JMLP&^i9QSR#&`(?8;9ickJ3-wmbc-4CVp$ZNjoc?+cQ7LlJy%x5qI^%VB zBS&e(O=$_1C})EI1K>R$D>!=_A3ts{O|5mAkc*Y2|&a|Ghu^ z4|$87XpRFr#)*dlT0>p;cbgO!-UfZI4N+Yl%uHQ;NO@%9J~`|9_Yjojw;@ig7~C1) zw^n;`$DC9%3*aQJz-w(}QXN=vck8`@5JrleZnj<$cM{u_@LsNo{Bf9HtO8IWx?La*p0B zrO`gbrj%k_|91C(?n$t7y})a4n2+T2PhJniaSs=KxXyF7R-BD68p?uv>a?;1()1%h zdi^oTzj9150Xr`I(e&}xnD>3WFedChKh$cEUwbRm4O}?Y=s~oUD*JyXgT3FAR=xmV zY0IUsFNdGuocTz0qF(M_zs=56ZIY!Aabs;4E{>C}q&8Uqf{(m`Xo2;(gjMI+R1w?BaXxzf$j{b_<3{m`>qawP<$q&yg}Le zeQ1$hl-3gE0iU?KArQK%j`Bzb0v(`ioB+HHj`1u%dWj3o*LFn56cZu3-31HO#S34X zrSjWE1HT zr9_sz{-VEnu59;+^%8G0nR#t5U~RVGP4h}fhrhoahC!R^I(IEk3CUu+TrstO$=Hre zE%gO6*Vba5*u=eN*bA#~JZvE$&&Qi^&EoKO!-_7)qKYJTT{?vYO{mt-joa;7yoYp2 z+g@ZyzD$8$h0(sU2jsPYEo~+RqiR$(GWFDGyJd21e*K4$gb$m)XzWI$XI3KC!c;GJ zAd|E`UG3o0szE>wWXM6&F|ve&9H2l=-)3oRW7OjCOB1s!sjGKET~k5g)1ix)|r`KSto%U2K(24n~&3N04n@+2R8+nTPamMeV@hSJZOJ z%$A%Vpz^eR?2`DHMb}*%K8^k*GMXEZuphAPq5NE;Th>D5Q%f;cTGyj6WK2Z+Dv-s- z16z^?LG5hhN2fQ{=BnlTU9~kfS}~L@VvRXN8q>P;=K9n#24tzaghqO}Ecyo{N7>8o z#!j=$ptu8l@l3;I8j}P;zXtH+V*CFP0<&P*VKkSm9ah_$FQgt4Ykcr8@sZTj7dh$E zixZq;`+#Saz{x=UJ3E!O~=wR&wP1e(LFe69Y~46V7=b<$0_;KO%;)N8DV zAvU?f6|Y>I&L2ZQNa8d$J$zMHAO2rnB|v|wx$inQ%;7jsc(`e8T)xgrhSFjLQ+>x? zlf0%4%lLzCB>JhTAM6ul*sQ}{0MrEN$79OR1PQc=3|0Vpy0308?D228=a2PqUK5R9 z&_K*Go}}dh1lY2~ykL^NKBky^Q5`>V^nP=VB0M;$A6#oWi~*<9skNm`VBSdi>He3i zfR#gWJ29@~^_E}v?wZ#?fKBSX-y3q-^`pVvUjZldmBp@MsqR*5biS5|{*Uu)jvhLm z!4to-;;~jJl+`ZYq!z~droKeog07x_>QY_e20ghJf$vHkoH+C#59e*B-{@sL%8N(y z8iJeF*8O!?_*!0vTfK@Hd_5mYqC5%kHTQML|6lwBI2HK?Z&16bmKr8h9-lWMj9`$k cXkS7yCEIZpIsY7ZbLgr2QVRF-B@O-l53}*ecaslbzSFq9_Mi!=k@HinhHHF2kpLn`{=K! zDrxN7N6rNPPeTs;i~F{V^1gkVFRm#mXt~7CA=@91HMC|$7T~c63*__Jlsj3OddCk) zgzPtL{%~qos4hyi>3T_MO}=mw@xSL;=mi3Da&v9XDPhu{v)iwJY$^1cNY;z0b(ikP z=gjuWeh?XNj-|uvC&y1y#Pxc`E1D_pHz z4zLzVgouWklW&W)yxNJ}<4T8)D5nW{^QImWI+Zmtv`t8RXAlMaJot!;7{PkF&ta|% zV}gi(2(>A-HQ6l6u;2Z&oGk(lFDhO{Xd+D^{h?@@9ge`rdyj7(%zTKlh$^zbHxQOO$5bPzbn&=6G zE7ah-KH?ys)oLKUYx#)Bf^^%8+v3K;M*UP&XPv2gsa44lQE%>n|8@$FxZ-;w&=3z~ z6}@=Dj?2x4t}i}};LZl@lV;72mO@3K0?-mGG4?ja{CxA} z56*?Yc`o{~P};#0F5^4F(ZRdhF4+WZyE+r`an{VFFD#dtnCoGmGn^|??&n!H5*EvF zGHo+eV-;gOX$CsS{N|Q&=8S9ix!D;s#)uu+E>xB5JTO>L=p=1V>qB|M;lmlJTVyPv zPI>}R`U>+MPfg$~uw`!U1b<7swOJrwX*Vv6z5$h027{17J3UAd3`5SiQ}w-vp!5th z#3g$=CeE}vvoU#XCw)w;I}=Au6$U9xi5GGVG904@&uVZ1(n%nTkAm$=tz?T85LGND*M4^yf%h%xzXf#ZAPDd3+osd z-NzZPpBiwx4D8!}`_!Mv!g+ce&JccYlT2o(1QS~3OjueFzBAA!5zy{5wCsn4ki!fe zFAr{6g-a9~WieXAaQ3uhKFNX)z|W?q?h=eo{ps{LSHpoVVSJ%rC}c$jO&v-IA*P0d znEDcoJJbH3RW<^XqB3O$j=-zPG-qIskBKhE{*h|G#; z>NKW#wDNo_dSnPWL{)*-E&u~34LlZ4pC^4|sSLkX+WN*Eh??!s72=+EfoC4B?*z|; zl0kALkK67Ps>b)_Xwv|vS;F7@vPR@Z;z`_Z#j2*cDkZHaAPKCi->e7B&|rR3`<;nv z$XCfOdEqd)C1>@U|F*KQw3#w)p;Pb?s2AHqFmW=MeRsMVrkGE?Cbd7=B1Y4(ibsVu z>rof2Z;S9@+nm{t}vRq*MQe)4)cb1>e}B`YUOk}`C2r6MuU$iP{C%y@pC zd%r|8#y|&SE;?6w@oAL6{OrQY^3|q^mIlH-U>F@jbs!!Xfn&WyaK>c|?p#Qz!~Nf0e)|8g^V9e0yu` z)^JcSTGIzy^HbB7t7Gq7R=4unZ!(}e?;Kv;`7mbdF8k*;oLl{(n8{3xOkz4pmwwUT z%~BTJK-!0pwV|m3VWGq0y7%#$7Pr1G{5V6()`@lVyvF;Pe0EDc^F)(eoc`vd z1(`a<@=c$%)A-~xXBDx>|{4NQ-81~RIp|(6Z+0n#gR}I-zp;2h>W&J2g zAidfr=U#_r9}m0-7Uxi)IJe&I^m_x;{_{-~dH$hz<<8N2Vx@J&BASN(=|ok1t^F#z zixXF{y)2le#lQEU#1oAJMF}E7YE9%!K%!eSFoJt8E-UX#S;lU;59c|?eJbLtp^;(p zo4Yj+r||HYYRyq&{QR(6q+Wo6lU(O;&zZI4+mH0wqBFyQm9)CYXzayj;J$KE-7~Ir zrIcF+FO9eVXt7!!I%d>g7D%=_jEMkicsnFsVe5*2;+t$YbypTWIF@5(f5xWpK z#N$~~TX3f<>p%8#y-QJE1p9>;Qm~47=V>*b*(-nIwuPGnq}+@IyuyR?=f|~3uoCZ{ z9TLJKR^g#da9^Qpf^7$9Dx4YLtFr()BXUh#yMi638AnC=H24Ql5`}Jhzm++jJCPNM_Ax-ksH#Iru2cUHs~4g|yJ(Yn z0mALBn_%uaP}>Gu`9YC*dP_)3Z7o7_9lklPt&;(409e|&neAv&4nLGNU_nMv@-|m{ z8Yv@>W3Ez-j2jGmZ3JuGOh#gHs~!M6$24;w8b3G?90F-!rmmmx9~C(HSFMig9CrMW zE^I@mRqBdJoH_VJxQu0^cP2P-b3SZLJSJrA;#LEW9jfYs?(wH!b;m8?52LgLxz7sT zsIf99Q*Mqhxnom>3V0W8`^_~00N99n`H&JFcfb0-kbq=t(U0NH-sLI~4S@M9C@lLe z!!I;oZe7u*jnn98_Eib9;x_!1298>XI@@4*slGTKl%MU2GUNR%?fzA|z@*Fdc_7wl zQSu9f&*IPQ{O6B>@$`JQf*PzcZ*JQgx)ZTjz>V?0wQDo7Oot<3@|_E`TL zIyI9QJz&B8Y21tUd)LY%Q$%z{EU`Wi$uZ4@OSXm`vzT_Tgn1Y8w539;ux@P7@4u1&zYOSfQN! z9SOCc-Iq}(C+-0a*`k&BKp&=(!|}NY!7Dsn{|PF_p;QEQ=ci&Ew{DkYzyAr~@Q zgR+FVLA-Us`<^7q%?!+&UC{=!CMB(xHw-qYy;Me)7P4QPi+;vnAz|dBbBIZ=tFFqm zUrLWD6#Fmo0sja*l$RNy>GrxB3mF{6*B349b_;&eZmZsV<)GI8Q@=gTn%F&BvQp>@ z!*NTM!OY~206eQrSJYMl;Th0CULar4mE$)wH;rfOzawf>sBnW;-*KP;!!P5&?YqOZ zFq@H(FgM{ZmoWX-pVZm$y&a-{R)+qzfHCk20;7_c_1hkti(^+93YErVy7`Q_XBS-H z>bob-ftpYNd3X8mB*4N=gma)%{O5CGI7^RX`uzV^A&?6(cmGN(w_Ra+1S2Qkk7vF9 zhPC%lqWN~ij0Lt*7eDpSmT4FCeR+D*56O=CzN`th^Q4lfRs*m5;5kjkv)eE{qkCR7 z*4$&b2t-5>-v~avCO$9Mqd7_teL!DftC$~rA#gr%hYd|zn^&Bi(rr$6t&w8y~vHs{K4n+U^N@L)Hi^Mxl+}><0{H!Dj0x zS~*P1CVS!^$4qB`-Ocv?^-DlMTOHR;w$n%$#rE-X=6ZL2tooW|v|uOy4oqnd*BGvp zx;@>exTt|k{O)MGzqDrOV6GxBmiTof0OUVBMb9y;d}w}Tz9<18 z`R7MRS*ud4T2aNrXk{INd)fNub5HbEQq>7OIr=j5N%bzy9)PiSiUYO(wATwCoQz9z zKxew1(+}-S3{oxwcnlZy16or=y|jB~8ob2yZDN<({bF0Tj~a{hT9^tqA0kX8K@B0{ z6n!Qry-Ctfi1}I&m8bmtM!#|BBiG!DG^r9e7586w^f%m)cwQEwLBdHyQsrNNsG<5eqzO?xN2Kk2(mnqd-smuTLkQpMvmhuL+nUXeU z8ephowyc;=Y8zw~j|x7okIO2sq&W^fl$CH)Ijz%Mw16r&M#}nWslRiBflHz-5T8)O zHFu<_Qysphn?0K68Q1`c(U11FevIhUrC>YRbhnfF-Ou+N=V4n?eu{(DOy!FuU(#B8 z2tA`X_WndORsqykXqFD4h7n~D;`hWRJOjsAo*2G$G11^{PHP0gZvnnN2Jl9-1Fx`i z&VL=bW8W4$ zrbn9}%pdl1e$}ZYc9e-Nzr$ba-#W_otYL#qv0(W)B*C)r@*4nIx(cLbeG^6IH&oHs z=1RU@o^Y3&1EQ3eSeI$ibiK&rCvmB*pO67#5;^@iAL^o%e}W=M6=+u#KC)MO&JhM2 zIBN54&W^jM$#?VWZ)ke&OnOF~xg?&`c1f@3+RwejCrxmKU0wW_mT86)*}_+XFuScL z(zSU_rcF7~%%DQ+{AGk=N|xBw#B_(9wYG3--1;0vs}idY0s^g zH}Q!9r^f!f=|bb;nKish{C>7v{Y!e_Ar9pOnYzwt#8F?TCi}2l5eFQfH--gnz(A>- zmg2oi>*SQWZLuuwD{b}bBu7HldF<%)*fA9GE4iQ4;-SYwy-tFCb=So}IM~mbFzJN} zKh;(bPWon984WGhewXg}d@ihLal%OG!8oa0dmzLA!?XEB?uuON1=~JW+rgnZDZbM; z&QW@8OO1WQ?*xmGO&+HlDB{V-v1d8yeu~gNaB#sx{#~(@*}H+K$Gtf4npnt(!e@fD zJ4q#nz8V~&+^CW>#jgOVlyW}ud(lwK*oAK|sz5Mod$Xp9*v=Vmn#*mB9d|EUP-v`z ziHG=QT>{zSg0-~U2J%H1a#H=`9L(^DDXXho8(tb(3LVSo931`yj6f>uUD}s=E3VRk z63w#w_}%@4z73RBYwvEXJl=noNF$g3cm%6^U!UFjbogg4{s+Pi#1US{1QvU16IVN$UU`bpp*`p9n7#`Yp!5bZuHGYlquer~Xeu4p zs;T{dK}X^W@qappCQl`jPsH{H?k}jToJ`p}u?DrowGo-`&#ukEPZyRl`D*qP0r`su z8*D_@S_sj6Ep`5{!fP~T7HI$gKp-+jp%OU_Gq~IFq$k1LydDDVB zwtI2T2TWwY9siEpQ^TCe-njLep*uep&K%65M6lIfQ%bo%(5J*YioYNp;*HTy-Y1_< zebWLavz{X(1fLyZdm&?y@s6?R-%t8)r?u(Q=bx-~&42X!(bA&;;1DOy=WRJu>&T{yh`U?-nub6bC3{s1Hc$E9`ZE+>|F4wYj>aP zDD=UHRwViyd6g|0r-&RX2iVujo%(Xyinw}<)MH?~+7rA{Mat1jeV7v3ENf7iIaXiQ z8TTZc`H3}Bf?6SE4^L70CB;fFdM=7W>IfkW#_|x%4$IW_Vad?M&R)5=xg-8^#xuj? zgWsnjTkv{6C2m#IR!@6IxHaw;_@3wiG?Y5@_y!=ljKAln;-550VE?mo3EfFho${+4 zRGDoW2)ViNDd_9~oOm+uZ_0qqrFDdFV`dQd;g=*a*b>D`BN?UR#%a#=8^aO0O~|${ z{UOEoK_?J>7}HJN5J~28%iDxLIcI;q?mmAkzR-DR9E(FbZ?#}McChUc*j5y_sTSKX zfc>|%t-xJu3`=(f74YrTSEPGG%~6{(-wk)`5}+7?VNjU^shiVe*dRf-%M1^1 zNUH2t%UKHF*wCKNECD+NrFt!qBqv7wRCDGV4 zd37?GyrAkMvLM94GUd4aDNi+8r&VKVC!04WRkNm5mi8BnZ~DYvw?P68<6%tCGYH*} z_E05|$d092vW6QqP75irDP}w|Q1{MkCpX=k=PIj??yai(F|l*QsO1-0nZg>uYoTFw z_i_#}>PNXo$C=c2&K?xow(~_BlhQ);ynx&isa@;>P5nvWO6Qk2jSl*SS`atU`hGQb!^;; zqNqI^w1|1AkFmOoN|Eu9F#{b@8$BJZp=EG7YXmoJ4htr5@sxVOta0bQwpZ4LPan#F zMa)T)!lk@-DzcZm;~9Fp1rYRDBz|e3P!M{mFan+Q3#ZP@gofV;f^*mwHS{dD-YYG2 z^}det>2-^TDydkr>*wAxR2 zx<23QIliko7lqYwT<0tmO?~rA^K~x&9ee*46R1R80#FMC#K+5}UjJ%7hc*#bu-bpn z#eZNQfsaNHpbkct1{M(ABY2=teiL~o?*(dfL~7-CU?zQ43V^)ne-o|$Z`OgRH~;$@ zht)Sr;61mv^Acm(`{kp+(#~v>g()M-9TLw`{W79v2Cl|z$7!h1PyIornu+AQhv`PU*Z;{Lo0V|OCDNdPXVCF zPCd!>3cz2`x;tr~z&n3Pe4Ea!?g^-42o%74#kg;j?>ywzjeU5KyB@(gc{g4J%rbi+ z@u_MqXneM-;DxO$jSr=Qgo~^&3zBL}^YRyTdVx1cNbc1T76{+N`hNWL_ll)!fv~HX z1(w#2`V1oA@orA&AHFK1mT>~my#OgugzBq1G$y0w;TYGl_}~*23Lq|aoI5QMYt5A= z&-MhTwJ&Nz+f^~`3mp;MEz_;Fghiu;}v;0jG~QY!+KQRZF>B3d6a*5`Qs0PUKQ2( zsB9%iH8P$}LjR-NK-TM!lq2@1RA2o1HAszqV@A87L0-i}@jrbVCA|^U0zc@_(v%-TR5c6NeaLEN{9e8J$Kat{P+uy|cHAZSyTrII)$wkQp09RkK7(WNV5^$_v^lSW(G614KNJXeLCIiLu4ak*ejO9jCM< z;kPFH^Io#11?aaoR!~=Ei|l(GO@`T|JDy}|?!d#nvU5We*eEtY1nlVn#3$DcDZjG$ ziwUPd{QyvHZVSbtAs_SviWx#TY2sHFeBj7&9J@W+$>4q*7GgJHiC%z5bic4cmnJG? zP>3SF?gYQH*DL>^c7ClF5L@#SCS5J&M{w&Uu7(djin?$T?Bk!>V@fHDMu0cV1+&lS z@Z(R_>SP=kP*eAgybTnG`*jcA#2DX=A_+8d8`12c8H@QbOqb?uOsEV_c*2rD-o?1G zs*MrDs6&$BVXaL|rzS~#HRC8kKXZe=D5j6f10+apCf}H)XzHbO7eWjfx6Ou7IEhJ4 z8|U8QgS_iS(rHBr~v>%^g5 zN~>`r-AJlWHjf3s?!^UJ*`2@G*L~>qvO<@nRGi9VLhYM)ebmY;mjp)xsxmSGso3-z zLGvdN!Grf4`UYbWmSSTaqr(-8@Td=C6DGpw$xTpL0ki!zv& zPPe=D>Nm*^{Q9IeaW>FUrMo@0^=WXXYHbL&{&X|Xq7c|`Vr5$j8vZIqYRCYZ;)6p} zcM2+jmuRtK{bpHc@6&GzpO3=gS)lZxG@&s|x#iZd+E$x?9FtbpL3z``DB%U$**<|L(_}Ts9 z5!FCgm>UE!cI-=V*}2Z>NXDxBDT&ovWh{5#>IWxZ0j^>OY9R(|!VcY|?n#p&9wQ{a z$O!UYc;)WZZK#)Pq^Li2lhArMvaWz=veJp?uv8Wr`Z(OhwokfwUw%W ztJ&qs5h#A|t-=iZu}S;af7#BPk(kb2#z=HLgR zbcf8^4(9xFWdMzFm?#bd@f&>3YNQ87>T6{kpoHlnR7NBRu9tb4SPg(Q@a4AeFeqBr z!>(>i-=nNw8uT}jJRH=77v#hKWu0L|eq?VWD)g$tLU|2@q$Ngy}_zfAh zqf_;|a5J3{bD2A1jxO#2d?Nd-nIBSgF$77}=;_L{{Mk39dpS3(lKtK`6->0h8hx!x zM)Dl)>YUm6wSBRB-+hWdnjH&pEA;VJUHV4UuA5z;9TRSDe;L0$Gj5wTJblt+wDv<6{Wz(H3hDX zx5VV>n@^Y8K9hDD2n>IG0Hc-rG&L56xn_Y=&0dQrxw-kpHbH!+;H)x@<#3{7YE zT#GyQ+QRx|E?Nw*C!e#KZ;E|ZZGlSYv5sy|4qe{Cc+nFaraf`r33{_5P)Tz_p;h6f zR^KNj{1~7A5~qYg>LSd(p1V{%6X!|;Ghd8En0yV_A)1p2pS#(Qf%X{&6L{{g@D5x< z0saT-4`xAUX*`g3#{S<&DM_3)oOr`cn|WEb+9_7IKz`lbY~66~irJw&=iJQ9c490j zHy{CN{T8sfAki^s2Rl9qaq%6YzKv-spKCsYomcCAxIW{hp1i7{voN+UP3*pukIbJZ zc3&nAi!q{@rU&W=Tc1|QK=Qwr?$UeGMkT#y0of%@I8_nfH}||hNV@5TGcr#NGP)P6 zUI3=J1*)b-OQ+kP+&CS?K=>vGNpcTj~*m`59~YM%1c842DxDG^HDPXfSBS-`erjVOhU(qZdT@1 zqMMoRgtKK=)ukof*5F7)9IZr)$VQ!3Q^WdeMfuFaT>aNm@mej+`3O0zIzYc4$SEB6 z#^7&yx-9m60F%QlWhJrh;oHeT?X&`GXKxG>T%r2?Mx=-fwSDV{u49ral?_a;?A;AA4=60UzsY}=|IU>6DIc~9Zv!8_dm zA1pz4cs`+r>T7=UQ}!j$>QcwTEsX9lapq_{44Pn1bQLJJKi~qKU)!gSC?m-*>W79S zhOqw{o^xoxEJ&MSueTSTfTp8*X!tmRV0puf;b^jm8nQdewEK~5JmrD0^LXK+@r3H)Z3Kre77ZWM<>P=KF6!nLGkw}S&>zZ2ksJU+ z8ZsR5|3+L~$hi7lO)sMsN(mO%@n)JWug6bnFM7}cG#B7B1b7;;UI+Eri|C1|zJ+JK zj!uP0r$d_40-(?9cCAwGxTW&F^9BL7`JpkQcxR8r0LcCaw8O=*QfG&3U>#B}#m>$j zAA`Zoo5hg-5t4V#QTjsJ5g|9|QxG_d#%pYH`nCn<<>hevNBfSt+#AL<7m45P_-5$xLDn zy6r!_b0KuiZ6`J+PwH{F-!U1o#g&szS1r`#6OE$QPHPylwgXk&4;{;%RqhNB?Vlpx*BKxMPDk zOlI1pcJMLG`hWzSWM@KHG~|4R(t*|~34g{b8j;B%F^aO3B$k&3W0)Oy5XM@tS>cI} zVJF&nVht8wWR$-GsLE8G8Nb4SPD$OkN!N;7t6)%mUbedlYK9P5dTsYtVx#Gl-KSS^z}jUR=_Jcb@m}kU+WVoRP}jZz$@CR5(e!$Vph;3HApV zQ_1hoJz9_2PN4zCz+&d!l8d904t-v6<$>v{JGGf%s_aK~8!V#os2BHl<*HsAkFB!y zMXbZ>1jRf{mu)r4(uygEj&gPlUmacV%b8Qq>^b%Qd2J`v0Q0dxZq7BgIPz#Cu6wD? zNtOjLG0vb;2N=LKN}o)I>|ZF(0EB>qLOJc3_VI+CDZiGpVN#BfU+1zruGbCeJ1Wun@IvcI4q7Lf$<-842iGy5cy#6n+gr*OF$&T>wfZE;i}4 z6HwQ0GIrkdAyPnpqecNIOB}?H)Uy)97x1~F+B)v)a^7iC+Gx1rsWMW$L6zsYAttz` zdtRRsY+18x6Sd%byi0mG>{*9L%-M{J?`u_vcMFP%_^_&6GWs`@iy3B6$XW@5bMh3) zP$R!#dl53I(huk4RHXdHm-L77c)?AfvOpn4XhpR7>Lrph{eMEsy9UkB%#|BO-RRVd z2*=ldTA}|h*c|A^+@6mQB(|{oy*l~APLqpsI5RhKD3?;1k?juo@OX4K>+Kk=FFmzS z^P*y+3-Re(eYi(X;>Gf?Eht0C0+wzh;AuF)N8_4l|7a5V;Fv zIeuJ+*R6++?>Pb@-#$Ls*`-LDDXN|D0;Ue~L8VA3x=@N$@4RYaJdj#awuv*%I4ncjiBbc2;3- z--7{3LR{Iq-sCfBY^1Q(6A2i+-B|fS@oYh`tT#_?fbw<0?t?I3YI3OQ5>mZ98a~|XqD5P zyoF#Z0i{W;`}+B*?HU(Os|pd#$%G%foGjGg1!6-Aux6YHQk&VQ+l(!3)eBE~O>X-L zX}{%Fr$_??KTQ2WW2jQ9*zv?CU0wRUQcxMtyne;{ERd1xSn>ocDaitE+! zLIq^%<=p{CORO9bFu&^awbQt0$GY`fIL8He(W9}$>D~SLYPF{wV0jCsIyG+_gy2`d zpbM@fi9;&!gFGjp5aa&Gy4fnPaj-N7acR1g(Ayu6WxZp<8NJnHd_V&@FKBO`*RJ%< z;DFK(-Q$6nZ!c^NuR+ZF)x$BqmqISZu=`&r2BH~wTz4I3PEx6f(Oln7ie`JmPS;2= z?1-k~7A{q7>C$}pv(`1;c~m(Nn1EZlA@T6?wqZ~O#Y*Jf*aoX6zfs{WaIlKG{N18) zZ(-%Nov#<^j<~#G3DATP%M3Z)1qW2B(vEJ2E0oAPZIyhfPrN4v3o8p8S^WH9hl&`K zNB#oghrv=uxJvM+7aq|Hr5-Q!%F=Mi-TA;)F%tXo@-dyyJ{*2sUADywz);Y^Zi8jKZ%o)19+I*Wpzjleq ztCQSo&_9|EFS?x8K@iT7EVBG#;en*!tFmo%Z~%RD-kk@*N&l^5-M&;voX^XS2;N|& zn7N#4!G4k#F|Pm$P^>KAybYu2TSY16o00JIpH|%difsNuN?`VDSa5Z&&}h8;%Y!p5 zunueHh>%Jt`Dc@ofo^3YGfLLHa5mf_u-06J*IB;ESSddx4URBHv&k2NZEdt{N^g!X zQ1thn*C6uFY!V8xD7GuMXDmz&PrQcKJ@m5>ei>P<0EV>O_7;A1y3{k+X~+vJ&*tu; z)7pcW)1L#VrIGn(HbA{?erdI>LZRN-^`Anz%Ri~r`(|-_QkJB50m(`#Up7XZ;sI0^ zHYOROKP$SW4)kY5D_U~u$O>sa6{i%;IpBd@;&2H;l(*~zANRjk-&mAT2?7s5xo5Mt zduO&*dD91UpZ?S~3V$Qco7*h&SYH1{adw)a2=M-n+Xa^(IvG=A#D*qAgoa;vzwTRG z^jshJMSr86vKGgZ0;!*GFWWI_Fwpt;A94vA^St$=7M#JDh~>F-1rPK>Z%p7%m26<% zJ7w~!`NS>Tm0f1BZ$MQM=xh1b$seY@!4?WL2H%S@O$Kxudza$nGbG0OG%>_;o)jg3 zAr47?936#&6#xZ$*N6gyIABC+$LX^lg)8sqYT3L(1*jjuEgX_;TbY@$+HPdr>NYKl zRNJuqLEVaST}6~))EiQ!AbupSWh6=G{ZP`fy62gSA6hCpHEABz&MbpM2A;<{(Q=H4 z+YMO>TsM%rcGoI$WzZ=gJYA90+61_x`3Rju{&M(}S)zWkvICT;fHMm-(1oARJY)L9 zr^@u&{ec(ic767P>1^lZcHE7Iv}?nYsvG>O88hB}tlkz-o$CWWkGA_G9)6>F6OXHbGf6VHJ+GWr{#_%<0{ZeW7TNyJ)Wp;;pf*pC$ozzx>( zsMK66^yFL!Y6;{S@C6kpcC<}NR&Jv>*yH477f5AMae|qRM#SLQ5t^%(j;}{uPq9CJ zEfneta5O{|uVKKb7DSb!IAq+fJ%0hY-Z90sBd#E63+0!ek)d`c>wX+IF+&&cf}}-t z7_N-L8iwN8-|^cG7ExqB?q3c|pjp3_dnc7cGxNs7Q1ABJGs%VP?Nib~PLY{8Gbu9j z1HR@j{M4Y6^?)TGc~i4F_FZRmzguG`P%=4JJ&d|$YS0Vn=+^#vz27jrJ?&@iqN{!` zW*`Mt@PmWERHX^pH$!@@iT2-J2=8=X({#m{rmUK^Ak-*az}fGS7_}YS5J+_6(<{w!dGMH9>jTT)|aLwGy_aP1(O5#AqCZee*7kLBQdCebpcw66By z#}kY?sbIJ)>Iw3D+6RDvr-By|4-{;aOk-U4r9Ji`Wrn#*hG7-8%C^B~y_8f^AD)ak z9?wE=m{KYWXvP}pK7~cD51n zcdfMM20-!NI-Z5Ka3`bl|MJr_$l&e^u$h)9HFRb1AkyHkV)EuHOm(+T$&5*$y8NW_O!XN621QQ!GmgSF@@56bY-3IXO>hNBA~ zjXu7&OPRAe8M8s@c{Vc_6cJi7T<80Z%!|ADGkyhgCvq<}bo*x?`PmKRg#NaOF!fiJ zZQ=R4`+?ySl+r@4(?ZuXU}~|7AO;zejRe2}F5}L~N>U%c@}7}sBS^hQq^S?EBX7M+ z@?fjoVhP;XWWc`S*}^%0l6G!)fBawNs()eS@aa;BJLcO|$i#7An&US};0}BLq;!ve z`%TVb+*$uM>ozWREU0*!H0bpTMR&kWh%$LSDuo$aAU!HGL((t+ArS8c^FZ&2 z!g1@6oE~u4K2*!*dI~UmL7Z6>>cHKwR|%5q-vSww(2eetnw_ALa_mxl`}tcofSunA zGD`iY;Q}U(aRMY~Z*a7Y_N|4L5YHVhgw4DlFbZ+#oGv$|2|&hy9MeC}_9f$@UN=&Z zBPUJI4fh@T>f!0b49-f%c6v1o0KL8i*z3PxeQ3b_m3e;~a=&<^micw3&x!`9!Zdx{ z&s8K0V#)n8LNjF_R9jK9tsq`PBbBm{$;V=XEWVF&0+ZlSsEXMy@6E&RgZMv_1v8?x zWIXx4GryTy_&;ETUZmYjOp?@lfm~O(zwt;=Z+%Wfg!_$;vHK;Slib>`z(INi(B8cZ zA568Yv*AJ0Us5ldz_%l3g1-SsLGk{#vWzzR@K7er?MUjIC(fNcTw9RUS80t{t2FdR z3046J0uz2gBwd6qSvQa(uIGb3W$^*v?c>qb8G0+3prh@T~+}wDg)CFEE204;_;af>HG#aDEiO{4zb?X1mes zf`ALw@689K?_f7=S!ip=U3-UY7?%eRDNg`$S^pA-;L9}!hhN2*u&9mo9r!X8ht$j= zY0lKpve2G|pMd5JC|MusNcQmSJqYIp*8~$2lUmzzn^5HQl9^B*JAf=wSy2)EJySFU zt+Wbm2dhj-yLj#D*jU8v~uBQa$ZeqQ|mPhqfl#{`B)%NTyqM#j6GwE$1 zru3^1m?>2P1>%bKW*47IcLoq&w1wm+aXM|M~?4N`GMV+h4;opi4@^NGY8`6rB##c*2r6%ki5NwVwnuA z;Kv8A6WGUwZdV+cd?&(&&o}~1!8B%w-e*#dx%-!Iv($IyeIomid{6zOpgFiJgaQ}^7}6%rGpTIPd>!%nWH*G{7`q>xiUH1|nyoDb$?v%5L~`=Y zODhG=au_(60$3fjSNzXJ(-lPhbs)8xgEJuO=-ez3)PZ&$2KfueSp41&^u~GSLhfv3 z@p0=I+(k~hoEsKb8cllrY7nQP9zq@`#^dX1c$VtIC7>ILscxpxP<@Q@7%z6L42ZP6 zY2dhc5W6X)KKElbzO$vPOXiC}+I6!0%#-~`w$`lqEc9<1u&JUM4~Xa47t%T~d+n3I zKu-9G>V9f6lxU1B3eEeCWEf+4Df{rnX?%lA)3x?_wc2>z?u}5Igv{(tL;GgfSW72V zELU)(3!+TK9;fN|ut!nGZ`cDM5CP{3I6O1~%zLOP(u4lXHkQt}w|vT5T?jM5tn$?I zgr1#)PhKUxaMw&c#>>N(x*^06t!Qj`l4Q$gXci#ajqQ_f(pff-={j*ATh9E?iN1{Vi4FAUI{h#6Z)lqc}30?zcJyM+zLD zkraq75Q)Ysb#7BiZ1!q4AN*mzt#a(?1DDofF!K#z(~oL zg-9bhnZ}*mBV#L1c#I2~I!Pnn06Bq;NGH1U8xU(1#TR#*6^~J9BJZfX>wJj|fEkpJ zu&tRGO`Zb$a)2<8N0z7hrah2$AV>Qk)q{+WEFa2G2`dS4_edm_wIVNHrhm{UmWMEG~mi2~4SCle-< zh8l}dPk~N~0?y?E8|Al~)@oa)Oxy-?Q?{`zq^_KOw^@<95;2n?_uCajJRa4}AG(Vy zm*!_}bE+pz_eRtk8uA7;gAS;i1R6bgSxCo_h$oO^q&L$ zF^cpNWyEV*xEnvKTk{S?RdI;!<{qIj~WwysmK;ns%uq3VjWSf`pF zxR83+k~Mpf;pieV96V`ZvlfA}0Ao-h%>+(L%k#PrC#C<98Cu*MR%vyc18`XwiuW_%094oD<#3kytFAr1A}R9zohO;{gE zmA~VEo%WR4w?Sy}297enKUe|$iC%3!E^RtaOw=QWnQd;@tv>P1CGFh=L$uTZ< z`7z8r@Yv%^lPxgg5Ge8D&`yQD^CU0{w3KlDyKRG)rPfAi`uepyn~ zk%6F0GQdPbQaI!ZBVNl_2+b->4?=&+mi)gXK}fa(5+e%y)@76VcOIWl&%{YR8`}81 zvwV9?H(ap1Uu)Gf;}=CDa^jr&dZBRXZ%IdF3wJ%r!e>hiV%}*M#!ay

L*eghq9S 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 F{vQsP3wQtk literal 20315 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 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ea0711ad5..a7267a87e 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -355,7 +355,13 @@ def ellipse_various_sizes_helper(filled): for w in ellipse_sizes: y = 1 for h in ellipse_sizes: - border = [x, y, x + w - 1, y + h - 1] + x1 = x + w + if w: + x1 -= 1 + y1 = y + h + if h: + y1 -= 1 + border = [x, y, x1, y1] if filled: draw.ellipse(border, fill="white") else: @@ -932,9 +938,6 @@ def test_square(): img, draw = create_base_image_draw((10, 10)) draw.rectangle((2, 2, 7, 7), BLACK) assert_image_equal_tofile(img, expected, "square as normal rectangle failed") - img, draw = create_base_image_draw((10, 10)) - draw.rectangle((7, 7, 2, 2), BLACK) - assert_image_equal_tofile(img, expected, "square as inverted rectangle failed") def test_triangle_right(): @@ -1499,3 +1502,20 @@ def test_polygon2(): draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") expected = "Tests/images/imagedraw_outline_polygon_RGB.png" assert_image_similar_tofile(im, expected, 1) + + +def test_incorrectly_ordered_coordinates(): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + draw.arc((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.chord((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.ellipse((1, 1, 0, 0)) + with pytest.raises(ValueError): + draw.pieslice((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.rectangle((1, 1, 0, 0)) + with pytest.raises(ValueError): + draw.rounded_rectangle((1, 1, 0, 0)) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index a55ebbe8e..2d0a98765 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,6 +303,12 @@ class ImageDraw: (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy + if x1 < x0 or y1 < y0: + msg = ( + "x1 must be greater than or equal to x0," + " and y1 must be greater than or equal to y0" + ) + raise ValueError(msg) if corners is None: corners = (True, True, True, True) diff --git a/src/_imaging.c b/src/_imaging.c index cece2e93a..12d7f93a9 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,6 +251,8 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; +static const char *incorrectly_ordered_coordinates = + "x1 must be greater than or equal to x0, and y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -2805,6 +2807,11 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawArc( self->image->image, @@ -2886,6 +2893,11 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawChord( self->image->image, @@ -2932,6 +2944,11 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawEllipse( self->image->image, @@ -3101,6 +3118,11 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawPieslice( self->image->image, @@ -3197,6 +3219,11 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawRectangle( self->image->image, diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 77343e583..82f290bd0 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -85,25 +85,22 @@ point32(Imaging im, int x, int y, int ink) { static inline void point32rgba(Imaging im, int x, int y, int ink) { - unsigned int tmp1; + unsigned int tmp; if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { UINT8 *out = (UINT8 *)im->image[y] + x * 4; UINT8 *in = (UINT8 *)&ink; - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); } } static inline void hline8(Imaging im, int x0, int y0, int x1, int ink) { - int tmp, pixelwidth; + int pixelwidth; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -126,13 +123,9 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; INT32 *p; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -152,13 +145,9 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; - unsigned int tmp1; + unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -173,9 +162,9 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); x0++; out += 4; } From a4965a7eaa0476ff415bac345d9965101be10ee0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 22:06:40 +1100 Subject: [PATCH 186/220] Split into x and y errors --- Tests/test_imagedraw.py | 15 ++++++------ src/PIL/ImageDraw.py | 10 ++++---- src/_imaging.c | 51 +++++++++++++++++++++++++++++++---------- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index a7267a87e..5295021a3 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1504,18 +1504,19 @@ def test_polygon2(): assert_image_similar_tofile(im, expected, 1) -def test_incorrectly_ordered_coordinates(): +@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) +def test_incorrectly_ordered_coordinates(xy): im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): - draw.arc((1, 1, 0, 0), 10, 260) + draw.arc(xy, 10, 260) with pytest.raises(ValueError): - draw.chord((1, 1, 0, 0), 10, 260) + draw.chord(xy, 10, 260) with pytest.raises(ValueError): - draw.ellipse((1, 1, 0, 0)) + draw.ellipse(xy) with pytest.raises(ValueError): - draw.pieslice((1, 1, 0, 0), 10, 260) + draw.pieslice(xy, 10, 260) with pytest.raises(ValueError): - draw.rectangle((1, 1, 0, 0)) + draw.rectangle(xy) with pytest.raises(ValueError): - draw.rounded_rectangle((1, 1, 0, 0)) + draw.rounded_rectangle(xy) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 2d0a98765..5a0df09cb 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,11 +303,11 @@ class ImageDraw: (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy - if x1 < x0 or y1 < y0: - msg = ( - "x1 must be greater than or equal to x0," - " and y1 must be greater than or equal to y0" - ) + if x1 < x0: + msg = "x1 must be greater than or equal to x0" + raise ValueError(msg) + if y1 < y0: + msg = "y1 must be greater than or equal to y0" raise ValueError(msg) if corners is None: corners = (True, True, True, True) diff --git a/src/_imaging.c b/src/_imaging.c index 12d7f93a9..1c25ab00c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,8 +251,10 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; -static const char *incorrectly_ordered_coordinates = - "x1 must be greater than or equal to x0, and y1 must be greater than or equal to y0"; +static const char *incorrectly_ordered_x_coordinate = + "x1 must be greater than or equal to x0"; +static const char *incorrectly_ordered_y_coordinate = + "y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -2807,8 +2809,13 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -2893,8 +2900,13 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -2944,8 +2956,13 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -3118,8 +3135,13 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -3219,8 +3241,13 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } From 396dd820b937fa42f85a859acb303e29d7b37a76 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 23:04:21 +1100 Subject: [PATCH 187/220] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d5798d41b..90f97d89f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 + [radarhere] + - Added "corners" argument to ImageDraw rounded_rectangle() #6954 [radarhere] From 1a790a91f57ac764d77fb8e5d23b82669419e391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Mar 2023 14:38:51 +1100 Subject: [PATCH 188/220] 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 189/220] 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 190/220] 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 191/220] 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 192/220] 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 193/220] 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 194/220] 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 195/220] 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 196/220] 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 197/220] 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 198/220] 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 199/220] 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 200/220] 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 201/220] 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 202/220] 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 203/220] Release buffer on error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej BaranoviÄ --- src/display.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/display.c b/src/display.c index 227e306a1..e8e7b62c2 100644 --- a/src/display.c +++ b/src/display.c @@ -202,6 +202,7 @@ _frombytes(ImagingDisplayObject *display, PyObject *args) { } if (display->dib->ysize * display->dib->linesize != buffer.len) { + PyBuffer_Release(&buffer); PyErr_SetString(PyExc_ValueError, "wrong size"); return NULL; } From 29b6db4f8a271b0c90d2bf129b40f2f3ca65185d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 6 Mar 2023 10:26:30 +0200 Subject: [PATCH 204/220] 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 205/220] Run CIFuzz tests when GitHub Actions file changes --- .github/workflows/cifuzz.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index db0307046..560d6c7df 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -3,10 +3,12 @@ name: CIFuzz on: push: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" workflow_dispatch: @@ -14,7 +16,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 2d01875e7c9be1b2a550f143c542bd931fa7c821 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 13:34:44 +1100 Subject: [PATCH 206/220] 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 69325629742a08989424af5e729aaafea8eb6118 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 22:21:37 +1100 Subject: [PATCH 207/220] 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 208/220] 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 209/220] 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 096a8ea99e485c1ac264be9ade814b0e1400fd24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Mar 2023 22:39:11 +1100 Subject: [PATCH 210/220] 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 211/220] 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 212/220] 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 213/220] 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 214/220] 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 215/220] 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 216/220] 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 217/220] 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 218/220] 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 219/220] 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 220/220] 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]