From 4fc0a4ceb292dcb5856c0ef079c8271a324f8e70 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 29 Oct 2022 17:22:07 +0300 Subject: [PATCH 001/727] 9.4.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 43896fabd19..1cc1d0f1c7a 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.3.0" +__version__ = "9.4.0.dev0" From 76b99756e425099b650bd383e204c259b5bdd3f1 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 29 Oct 2022 18:24:44 +0100 Subject: [PATCH 002/727] disable __CxxFrameHandler4 when compiling harfbuzz --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1ef0..bc19c5fa162 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -360,6 +360,7 @@ def cmd_msbuild( "dir": "harfbuzz-5.3.1", "license": "COPYING", "build": [ + cmd_set("CXXFLAGS", "-d2FH4-"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), cmd_nmake(target="harfbuzz"), From eaee7fda973d6915f7071f9b7f7edb7ad481ef7c Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 30 Oct 2022 02:56:05 +0000 Subject: [PATCH 003/727] add xfail mark to test_image_access:test_embeddable --- Tests/test_image_access.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 955740b952a..04bdfc5aa8d 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -406,11 +406,8 @@ def test_putpixel_unrecognized_mode(self): class TestEmbeddable: - @pytest.mark.skipif( - not is_win32() or on_ci(), - reason="Failing on AppVeyor / GitHub Actions when run from subprocess, " - "not from shell", - ) + @pytest.mark.xfail(reason="failing test") + @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self): import ctypes From 181fad2918f6ca24f4f149ebd29af73dda34d2cb Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 30 Oct 2022 03:17:13 +0000 Subject: [PATCH 004/727] move import used only on Windows and remove unused import --- Tests/test_image_access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 04bdfc5aa8d..f19db440d6b 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -4,11 +4,10 @@ import sysconfig import pytest -from setuptools.command.build_ext import new_compiler from PIL import Image -from .helper import assert_image_equal, hopper, is_win32, on_ci +from .helper import assert_image_equal, hopper, is_win32 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -410,6 +409,7 @@ class TestEmbeddable: @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self): import ctypes + from setuptools.command.build_ext import new_compiler with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( From 23df3bda7f4e052c84dc71d3dbf417f0d8d03741 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 30 Oct 2022 03:17:54 +0000 Subject: [PATCH 005/727] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_image_access.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index f19db440d6b..6c4f1ceec04 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -409,6 +409,7 @@ class TestEmbeddable: @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self): import ctypes + from setuptools.command.build_ext import new_compiler with open("embed_pil.c", "w", encoding="utf-8") as fh: From e50a3a213ed5f5296deeaefe9ec0699010545157 Mon Sep 17 00:00:00 2001 From: TrellixVulnTeam Date: Sun, 30 Oct 2022 23:44:48 +0000 Subject: [PATCH 006/727] Adding tarfile member sanitization to extractall() --- winbuild/build_prepare.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1ef0..0e585f79652 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -474,7 +474,26 @@ def extract_dep(url, filename): zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: - tgz.extractall(sources_dir) + def is_within_directory(directory, target): + + abs_directory = os.path.abspath(directory) + abs_target = os.path.abspath(target) + + prefix = os.path.commonprefix([abs_directory, abs_target]) + + return prefix == abs_directory + + def safe_extract(tar, path=".", members=None, *, numeric_owner=False): + + for member in tar.getmembers(): + member_path = os.path.join(path, member.name) + if not is_within_directory(path, member_path): + raise Exception("Attempted Path Traversal in Tar File") + + tar.extractall(path, members, numeric_owner=numeric_owner) + + + safe_extract(tgz, sources_dir) else: raise RuntimeError("Unknown archive type: " + filename) From 74c60b47a89502adec658316b06cd0043046d36b Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 30 Oct 2022 23:45:49 +0000 Subject: [PATCH 007/727] simplify patch, also check zipfile --- winbuild/build_prepare.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0e585f79652..3cd841484f4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -469,31 +469,23 @@ def extract_dep(url, filename): raise RuntimeError(ex) print("Extracting " + filename) + sources_dir_abs = os.path.abspath(sources_dir) if filename.endswith(".zip"): with zipfile.ZipFile(file) as zf: + for member in zf.namelist(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member)) + member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + if sources_dir_abs != member_prefix: + raise RuntimeError("Attempted Path Traversal in Zip File") zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: - def is_within_directory(directory, target): - - abs_directory = os.path.abspath(directory) - abs_target = os.path.abspath(target) - - prefix = os.path.commonprefix([abs_directory, abs_target]) - - return prefix == abs_directory - - def safe_extract(tar, path=".", members=None, *, numeric_owner=False): - - for member in tar.getmembers(): - member_path = os.path.join(path, member.name) - if not is_within_directory(path, member_path): - raise Exception("Attempted Path Traversal in Tar File") - - tar.extractall(path, members, numeric_owner=numeric_owner) - - - safe_extract(tgz, sources_dir) + for member in tgz.getmembers(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) + member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + if sources_dir_abs != member_prefix: + raise RuntimeError("Attempted Path Traversal in Tar File") + tgz.extractall(sources_dir) else: raise RuntimeError("Unknown archive type: " + filename) From 7528b673fa4517604f4af91a6d061802843f3246 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 31 Oct 2022 19:36:14 +1100 Subject: [PATCH 008/727] Removed Fedora 35 --- .github/workflows/test-docker.yml | 3 +-- docs/installation.rst | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index c68d43935e2..1e36b338299 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.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 @@ -30,7 +30,6 @@ jobs: centos-stream-9-amd64, debian-10-buster-x86, debian-11-bullseye-x86, - fedora-35-amd64, fedora-36-amd64, gentoo, ubuntu-18.04-bionic-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index eb69d580567..4812b27cf92 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -440,8 +440,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ -| Fedora 35 | 3.10 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 36 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | From 5b4703d6153689bf008ffe37f43c489c4f9211a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Nov 2022 08:39:02 +1100 Subject: [PATCH 009/727] 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 902d8bf8fa9..0a7202a338d 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 2b45d0cc4a8..b03bd02af2b 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 6fd772e6694c53aeb1c9d5ff48d931a4db63fb2c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Nov 2022 00:08:29 +1100 Subject: [PATCH 010/727] Updated lcms2 to 2.14 --- 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 4812b27cf92..c6509564095 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -147,7 +147,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.13.1**. + above uses liblcms2. Tested with **1.19** and **2.7-2.14**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b4b15cc1ef0..455481e3da0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -293,9 +293,9 @@ def cmd_msbuild( # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.13.1.tar.gz/download", - "filename": "lcms2-2.13.1.tar.gz", - "dir": "lcms2-2.13.1", + "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.14.tar.gz/download", + "filename": "lcms2-2.14.tar.gz", + "dir": "lcms2-2.14", "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { From 6b286ed62f6cb2be447a5bb3a7f09f5edad5f3d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Nov 2022 07:47:05 +1100 Subject: [PATCH 011/727] XCB will not be used on Linux if gnome-screenshot is present --- Tests/test_imagegrab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index fa2291582d4..5e0eca28be1 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess import sys @@ -33,7 +34,9 @@ def test_grab_x11(self): @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") def test_grab_no_xcb(self): - if sys.platform not in ("win32", "darwin"): + if sys.platform not in ("win32", "darwin") and not shutil.which( + "gnome-screenshot" + ): with pytest.raises(OSError) as e: ImageGrab.grab() assert str(e.value).startswith("Pillow was built without XCB support") From 6ddbe4cbf029a1d1c33cbd68683801864092cb47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Nov 2022 18:26:31 +1100 Subject: [PATCH 012/727] 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 cd142e67fc7..0229b224304 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 1e79db68bcb..93ae1b0543f 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 c67d8d6bf60..11d1d488a29 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 72c7f64d0a3..aa47fe67194 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 d030b0c439e..b28a0440acc 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 fe5511ba5cb..db1c5c0c93a 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 c10c6bf8940c2a1bbbc1d01575489cb85e612a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 3 Nov 2022 20:23:59 +0100 Subject: [PATCH 013/727] use os.path.commonpath instead of os.path.commonprefix Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3cd841484f4..14f8d7ba0f9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -474,7 +474,7 @@ def extract_dep(url, filename): with zipfile.ZipFile(file) as zf: for member in zf.namelist(): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) - member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: raise RuntimeError("Attempted Path Traversal in Zip File") zf.extractall(sources_dir) @@ -482,7 +482,7 @@ def extract_dep(url, filename): with tarfile.open(file, "r:gz") as tgz: for member in tgz.getmembers(): member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) - member_prefix = os.path.commonprefix([sources_dir_abs, member_abspath]) + member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) if sources_dir_abs != member_prefix: raise RuntimeError("Attempted Path Traversal in Tar File") tgz.extractall(sources_dir) From d93b9919e338f8fe253e76fe0bdc7f0994267385 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 3 Nov 2022 21:40:08 +0100 Subject: [PATCH 014/727] Use verbose flag for pip install * Ensures when developing that compilation warnings are visible * Provides feedback that compilation has occured. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8f2862948a8..7783bd96b99 100644 --- a/Makefile +++ b/Makefile @@ -53,12 +53,12 @@ inplace: clean .PHONY: install install: - python3 -m pip install . + python3 -m pip -v install . python3 selftest.py .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" . + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" . python3 selftest.py .PHONY: debug From 41987cffade673b46b96054f8462f3c45d410de0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 3 Nov 2022 22:57:39 +0100 Subject: [PATCH 015/727] Fix compiler error: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/_imaging.c:1842:17: warning: ‘ImagingTransform’ accessing 64 bytes in a region of size 48 [-Wstringop-overflow=] 1842 | imOut = ImagingTransform( | ^~~~~~~~~~~~~~~~~ 1843 | imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ src/_imaging.c:1842:17: note: referencing argument 8 of type ‘double *’ --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 0888188fb20..940b5fbb39d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1829,7 +1829,7 @@ _resize(ImagingObject *self, PyObject *args) { box[1] - (int)box[1] == 0 && box[3] - box[1] == ysize) { imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); } else if (filter == IMAGING_TRANSFORM_NEAREST) { - double a[6]; + double a[8]; memset(a, 0, sizeof a); a[0] = (double)(box[2] - box[0]) / xsize; From f9a2f991db4fb91a78ba1bb30bb45b9d2b235348 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 11:48:18 +1100 Subject: [PATCH 016/727] Replaced IOError with OSError --- src/_imagingft.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 7cd6dfb1da7..bd409917601 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -956,7 +956,7 @@ font_render(FontObject *self, PyObject *args) { /* we didn't ask for color, fall through to default */ #endif default: - PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; } @@ -1023,7 +1023,7 @@ font_render(FontObject *self, PyObject *args) { } } } else { - PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; } } From 8947cbf4d113b112660b15db23fe84f94e128788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Fri, 4 Nov 2022 07:31:00 +0100 Subject: [PATCH 017/727] simplify code Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 14f8d7ba0f9..872e74a20b4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -480,8 +480,8 @@ def extract_dep(url, filename): zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: - for member in tgz.getmembers(): - member_abspath = os.path.abspath(os.path.join(sources_dir, member.name)) + for member in tgz.getnames(): + 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") From bbe9cc6ae5f19207b0c3bf80f591016ec03019b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 20:26:39 +1100 Subject: [PATCH 018/727] Use verbose flag for pip install for debug target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7783bd96b99..a2545b54e61 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null .PHONY: release-test release-test: From ef1eb2f3d6e7e19c251beb6eda5e9ac06e902ca8 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 4 Nov 2022 11:16:22 +0100 Subject: [PATCH 019/727] Add oss-fuzz badge [ci skip] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e7c0ebc5aa5..7a81e0c404d 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,9 @@ As of 2019, Pillow development is Tidelift Align + Fuzzing Status From 13a4feafb75b1c0cdf4821dd6db88f0c44d9ce4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 16:38:50 +1100 Subject: [PATCH 020/727] 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 5277b84f81c..9f1e74e530f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -323,6 +323,11 @@ def cmd_msbuild( "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 b8be24850bc07c04a21635dbd8835c9e8408cf93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Nov 2022 21:54:23 +1100 Subject: [PATCH 021/727] Added file to questionable list --- Tests/test_bmp_reference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index b17aad2ea50..ed9aff9cc23 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -35,6 +35,7 @@ def test_questionable(): "pal8os2v2.bmp", "rgb24prof.bmp", "pal1p1.bmp", + "pal4rletrns.bmp", "pal8offs.bmp", "rgb24lprof.bmp", "rgb32fakealpha.bmp", From 4001a9fab471dbeaaa6f530d98208e25ef6bf912 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Nov 2022 22:41:06 +1100 Subject: [PATCH 022/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6f2ba569e0c..fc8d8362a2a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,15 @@ Changelog (Pillow) ================== +9.4.0 (unreleased) +------------------ + +- Fix compiler warning: accessing 64 bytes in a region of size 48 #6714 + [wiredfool] + +- Use verbose flag for pip install #6713 + [wiredfool, radarhere] + 9.3.0 (2022-10-29) ------------------ From 9448532f913665006b0373ae3cee61c550b34339 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 18:03:51 +0000 Subject: [PATCH 023/727] [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.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0) - [github.com/sphinx-contrib/sphinx-lint: v0.6.1 → v0.6.7](https://github.com/sphinx-contrib/sphinx-lint/compare/v0.6.1...v0.6.7) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f81bcb956fa..2c13fb3b144 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: ["--target-version", "py37"] @@ -44,7 +44,7 @@ repos: - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.6.1 + rev: v0.6.7 hooks: - id: sphinx-lint From e31ca06b7cde667aa973cfcb35166d17a5408925 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 10:58:33 +1100 Subject: [PATCH 024/727] Updated AppVeyor to Python 3.11 --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 20908052bab..b817cd9d804 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,7 +10,7 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python310 + - PYTHON: C:/Python311 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python37-x64 From 5471dc2b265d2486a9fe0f37d2d7cbfe6f4f4cd6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 11:49:39 +1100 Subject: [PATCH 025/727] Use fractional coordinates when drawing text --- .../images/test_anchor_multiline_mm_right.png | Bin 9514 -> 9525 bytes .../test_combine_multiline_lm_center.png | Bin 4160 -> 4147 bytes .../images/test_combine_multiline_lm_left.png | Bin 4223 -> 4197 bytes .../test_combine_multiline_lm_right.png | Bin 4170 -> 4154 bytes .../test_combine_multiline_mm_center.png | Bin 4243 -> 4204 bytes .../images/test_combine_multiline_mm_left.png | Bin 4216 -> 4189 bytes .../test_combine_multiline_mm_right.png | Bin 4247 -> 4215 bytes .../test_combine_multiline_rm_center.png | Bin 4160 -> 4149 bytes .../images/test_combine_multiline_rm_left.png | Bin 4149 -> 4144 bytes .../test_combine_multiline_rm_right.png | Bin 4215 -> 4186 bytes Tests/images/text_float_coord.png | Bin 2877 -> 2875 bytes Tests/images/text_float_coord_1_alt.png | Bin 807 -> 809 bytes Tests/test_imagedraw.py | 21 ++++++++++++++++++ src/PIL/ImageDraw.py | 10 +++++++-- src/PIL/ImageFont.py | 19 ++++++++++++++-- src/_imagingft.c | 12 ++++++---- 16 files changed, 54 insertions(+), 8 deletions(-) diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png index cf002b12cd0a89eec1f97cd1c6530b7d1007fc9e..7e98b8eac8d780876d377315b2158f203c30757c 100644 GIT binary patch literal 9525 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 TfD8N|D1`D|b%jrICcgg%hStVx diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png index 7b1e9c4e42f69874b4c508d16bc85527d5d902b7..6a15130248ad8bed5e13a937d3667eb98624b5bf 100644 GIT binary patch literal 4147 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 literal 4160 zcmeHLXHb*r7G^;}v4E)PrA0)SCZN(J0iq((!$nF!q*-VxVQB$EV8sPgRuH6j3nERB z7D#|lMS2&31P}-B#H&Qs3h**_8bhXq6hxVX3u z8yZ}{$;I`{%f07*Zg54A{5p+`ONig_`c=z-bQ(GE;c-7s{R-Lo-ErPng={z8L&@x4 zVx5*{=ikQ3HaHEQ;(l|Hd&TJbh3|8*Z_JJtH=mk5VY#nwGg4SE|FmehK5uijxa5A> zFsE!cp$gu2V-t{_-F2MzaRFF))^<<}+{NG3g;+L%g~3eF1574UPPQDwsKu3*mgebS zHMSTWKaZaf{GA1{AB&4oix+!^a|WJyvL9Q@`&3ccX7X0IzkCu; z)fTv@ zzFa(hyRfYL7BK`XpWs2pGj#>EeW^t_krNUNoFFQd>KMGzl7_0QuB=o~Fk;fi(0y<8 zWYWDFzoYsz3-?`aJd@m=0qaRo$+ai==UI9Tf3(Mu{m1Gb^SJh8X0+Wqf_jo-@;*O* zv1YZKz}}{oRV;oj8Y5Nv#C)(m+jJBRX>;=qr9C0Z-rsipvO>4il_3U4C0CLf*Z)vgDB|x#{_nfrR0jtK%iRZl%|OIRYUA@- zZ*nO#VYm*wf3U--`@O`lm+rjS5hI1O<+OmAwDN}ULt%XlxT*HB(U zex_GqKmtIjqsB>tZ)KB3Dq)`hZlkrB4RC|hpyRv)js7y3K?c|4EAF;N>qD?>?_MFG z@x#tQkvw8{WiDOV4Klh<@1!4Ny!PF@nQwPvXrmR>mr}ZPJoV!V1#5A)BHJ<-3cMN2 zC8XCJHCism*|3Ndgk%y5`Uulq30ysL&o-e69)rp<5Lufv4V0v&_dRHarxW>mQzU0| zI%db7wOCNxt!RDg7T%>RH6jwbkdri<$mq}MNK~|A6JU;^Rogi;1(4V;LBFHK?6mboHQe%}ZhzZaj#}LC?Woj7c>Z zJ$3AoIx04>@DC0f?H>dz!#MGL8)Abx%xG6ACZV^U9h0d9xN{QCbhhrz_r6wBQ>(TM zVlG;loA&|8wzs#t(lnH9N=|cf7mbaKM3in{_L{7(t=whrFa#x_svFSIwLX0rM0yp2 zOa0NPrGo=|3`M-}imLh^vFl?crs0`>@$QXj|J?Gy;@RU4$M|0*$}@8OR=YIj98hm> zynx166ciK?(2PvGJ)K!uSz#1kwcT?7^z)~UQbedDWv-7N(dz67w8}B>j`;ZaVDQ}? zC7-r)Ni&HHLj}VOA}1vKlf)uDz~@{&QMg~64CYsD%#VQ4%5#j9X0z?kLv_RU0Q~cl2Qm5Of-yM#Z5mF ze#S->24}4e`1EKerC})vNW4ESC_B7IXJ_fR>CY6G4Y9(wxHv!!3Cz-`k&$!8iCVkM zP1P6;Pg1}R4L9c^W0o1bKH>^#+cb=J>RU3&($PlmQ}vziz50YtttL36BxLh@WP$>c zPB7uHSqxAEX(?XL!n){ILWU&Fe<@l5Ryx4Lcj!Z5p;lbOE7^uOBYFAx?WT@?LstCu zb&kZ4E!R?S*X9lHRtdDFrRBX`G-Dh~s~DyO5J$`1fm?Nqe*+E!YOL@WT3n7qH13O< zy$_nt3S|d+u6L^RXwAzn_(C~{SU~(61RKDD&J-0NcVHhQia63G_x^VzZbA62*F+sw z(1#>`B1~_!6bx!tWeOB#00~18S?ajO+W zf_qYOvqGSaic6yv1&=YUfBvJ4kzfHnJGHhjAXb-|IeVqV&4CS+B2Bu+YET*uo=tHD z#vlwc|@k*9aj!RD_mc<;_ zSTuJN237V0)7l+bnQlc>zZU&I=7{sOXNNOv0pZNFnS-VpWXpQ~GY52p`)hm``p<=~ zgOF01{0t25WA$t*DYqMu0j1DWRo!p0Q!uGA4Q~P$26D~VORia1>-rb)<$9hd5yf@{ z&Xc_THb$qN)nW!%`@#Gzh?in+9dTY68&zf67!E%Dr;2x3bpbSn)SqL#s_egKupJ#8 zxxn^c>qagq0s|n-Wnu)FUT3eC7~E#tu0|AVei2oFXyds}!%d8X5Q>d?{rQ$Tg+T%+pCVqa*IvjDe8`h(>YT~*6 zsZ~-zL7~HO608K>*ouXS!(yH_E1&r!b3PGtrPP=wz5A|FYRkPN)oocj8xu8wK1~mh zNF>8gt!Zm6ix{vn#ZtPBMB=~;7^9{P_=80*f=nGRvct`W!c5r=qZtes@hXH)NpjTb z{dU9;T~jSQsSsy)1De6%ZP1N4WLxFs?73bw9W8e8&h?&5?FSwnfC%eDNJs6pUfq+o zy{@wP>NcmMK)As-a3Qw28UDAxasyTdka9b%kkE&K=^!~3kRLl)tjWPV3!vh3V*Ash z(&NC1mVCi31_bJtjT#o!4qEF_CZ=b2PSmlUpHM((QsP-kfd+)t;?S+&CNmkd#ZT; z{J9;fw&L;K#MIQ({QUflaSQ`=cpGdEpaC#%cXxM?MnHkvYYQp?%ac*cb(cBiNMNG? zL;9t@S7thaV3CUaF%|~0w(EX@Re?3oiRcBV`j8~8FLd9<8ENMRuof>{DdxpHok$0Z z@6@=Q#Z4xYv&NBCZQyPcI)D1m&*LWqf3aZGvHpfgu+eMjX7JAdm!Tfw`p0WdVgCY0 CJA=Fc diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png index a26996c2dbe70748f834607fc49ae8dd079c4e09..8eb254fdf26323e778c1ff5e73dab8c8a0b4b638 100644 GIT binary patch literal 4197 zcmeI0X;f0{8prKaSF59WthA%1X@ljQreqErQ!~>dha5sAk7JsWBPxRDRyQ+qK87f2 z-5fIK2}d9^kBMU#ih+{moI;{thQocm=fk~c-LLm!do9+>W^MLvwCMG6nbIH;{Ol(K$_DkXi@X7wsXE|bG(wa7w=1x(>#Zk;dr--drO9X@#q9yvY zfqvz)>Nl5VTtE84PKS5AkY;tLYO3s0-xu?mS%0ZJ*wUpsRr_Mx{qT^Mgx_s_BCGAn z?eq;;RSexhgaLYHwR1ohibm+@4{|$a1Q%wBBl?^*B|6{YXn^Wx>p z0G`{Oy3+x?Jz|RLZ9lx0);#*-_vL#D{#yl?iX2LWc{|%qWXx^#*&WYL34gqGQl?`q z8`e4Ak%_u%)tBD2qEYKueD%teE7pb=y}Z2e+6eyna-pyvxi5M$Ra(NL>Nnk8iLE0I zDCE2{gTW|i52-t{-{J7x-S<^ofBDOD(7n=woWAASc*he1i+{Ci-=AOpKQGT6F|gmB zbB4;{EX-cT6Rqvr(hn6_soFa^{cd+MF)@*Y;gK(xyop!FF`L3>y!G6ioL1(F^JOPg zB+RScGK>-|t*oq6)BJ;j^Uj4#bSlRYIZM=aes^F+lP`yYO|blxKiM^ti}t{Es!6x0 z?pMMrRR4Xa_(_?BMOFhc{GAodWXV!JlZItFsT-iiTKRl_5Q}s|CSklizCX{@c-1pO zgMe3)-5N~qvQ)2+5zH_58KXLrkIb>Y2^U509brkzP|AzhOPj?^HhZ1h!s{a%>4%9i zi(&bNw)mpj!xnmkFB3FKo2uVdtDZ}RdvwPkm)p!LmF0}h4G}VWDiiM!Gxs*_7)H1d zJ&-p4`7>S%F5DFHsDdGQj$sguZ5I3W3uG+Hi&^ElI}6(;;k-ja)~GwiCwR0ug7*9A zeX^SVO$<;IvpXMO;WobI#`3!mEs>@|rWdFmnm0DeWH$PAKS9QAL@a)&r)~)0-Nsjm z=R&|nLA_!RUwB2e7YHC84r+ba0VmH@jpkF`UdwwyA7D5!Nxb99Z ztEdR(8A-#hM+hDd2etg*8!$WB=Z3iym=UxBACp8DTVh%L`00Xp!?ZVk2ESyz2V-ObVz*;S^(^BI#-Y(wGr)#L~`bt z;Id&4Ob`pHj9r_;qu7nrj22jyu1c+&jZN2w`asrLrmYV7Z0*MBVqy(Y#mXWFyE^L0 zOO|ul5fyJ`e%8=XiQ9mwcY?#=&d$R5(n>=Jq@j9*g`#4I-2AjWS{tWQ^02}YptM4~ zg1meMb@Q9ygKP-O17OEDg3E3q@P1~Ig5ca13vO($oKfJSsEphADOt%hrT{Oe?cR8M zq-G`5&25C?f*cKfd~2`>$kIUc1ZKA8&29{ll$0dEAN=^UUd+m@Bh1tw^D+RHmRpT? z`vXZre&igzkVFy^!}>;0lO2h2DMySAmrdd()2xfG(*R;+$O;z*CaYr z=y!da28lY9sHhf4hlxN6zcQCI9&*rs(ai~g$aTv*9}zIU=4@{ta>`<8csM_1zJ!1g z@Ho_>B427yc1{iwvsi!N>N%C#k&kWn6ciNT$&sv}1|JgGNcSoyjuV;#Zho$|H&w-A z^V{djoxO|K(4C2;Vu~{KpRVManTE~30#c=Xok+CRZt|uErFTu<-*aFLU~OVzVnyro zaAn-Lxng*-Vc<|1olaljb4Ob$Te!_dMMVpe9#P}}Ky$DVvb(!G_!&CNXm&_6`v~8v zYW9vHYHWRdx!=zji4`!$$QWh+PhtJ0t3^4%v@()>nKq)il~2>?R1C9=5iIvZ^Gz|4 z!uHXS&Lady3y&gVhxPaRY#vO$`EI-?2cokq8}^yQ|+&L7OW`L23lj4Udxq|nN? z#U)O*)g$z5-p2vNl?OWm=F@F;eCVXuH9kDJX$sh|=e8~YCV;f^c*s1 zPs(6prN!yI8ITxn*@a05ivITF;Z!U|L^KN16TkQAkEHI+8R+?wZU6KdX^jp`KDIDY zTbz`CHT!JI6v5FnW+e+K89aOG+3B;}*EEIsB_Au0ifo<+#dUFTNXK@!?}rkx*KFb$ zC1f?Rt$v)}B~Xco`S7TII)DlZ67?wwQUiMmJ1zsz+&KO~LF%);zuaA`PCBsr0^_t} z(aItVlJW_F&rI}YwJtk5+b=JAdO8Qy_i3lSgw&onS3N3!m?EOF< z>p`1A`qV<3ioIks&p+{Of8bMz1C)+djGgWw*y6RX=qQ7H1GKT9hT!BC6g1S;HvwNv z%oL;5T*Ech@}edPet879ULu3gqC;KR6M3(SD@8r6t6Kq7<^dIbdotCH-wxfim~(s0 z0reJOy)3htqgo>M7!=5kKav!}@-4t&Wz|8!is+@F#SI)BMm24g)qoNb-1bA)zoehA z{h%~|2OV!Bqq$dGL=%T@tPGkrQ0Izr^zes+0s?5=YsA5#E25=N<-u+cLhHaoS3XsZ(4^F^k+*+i!;9cc~TQAweJB3!!pKTy83C z5o@kJVa(jUu7?VuUj!NB;xa5u6$k{P++JPZo~(W|Q5hpCCp-Iz7CaL8YK6y%0}t1V zr%}%L`rr{m^G88@-{KnAmS>dxgX5cxJ=}MHlJP8Chm?PqqVrK8|(|)To{<)H>r0;7tjozUDO;Y|W03T=s`twi_um>nA zvevhkmZ_mH9s=Gp13%XC^loZX8G-BK$!Vdgg<1#1pdXThvrWn$aiF=*DeYd%D`3-~c*@5;LGv2CH zAeAQ_yCE26F;k$MUyq68!fD;zpb9re`KgiHT#J3sLG_#+cxGT=5D(7>!~&`yy?m*@ zjFk;KrLNxm<@LT=P7&xgY%XrEU1?caPi*7ZO8_-L1LjBCkqdns?8w{jMG`U91~)1~bW%-}vg_sw~6U)&e>WiS4p=kxh5&+^@V&+~tD)!tfO_Mohk zl$5;9l}k=iQd^#GUNT$39YuCFSxRd6Wt&SEu0>H6*#38~`HVG<6P1-7tHvMx<)-qs zS7){M?$h^gv(|FVX?}h7KCShi?K*otwleSBJQx2}8MEu)*Z9>7Nph$Z+uW^Qp zOOq$gH6IQEPl!Xg)+vL7gJ!C^d3kxRWo|n%TwKM|We>LU(s%!-wC z$S$v`*L-R8U*z@=+WML}f8%V(XkBTh!o6L}yAwRNc*^{ro69MsnNqmMBvnH+U+FU} z>cNithi*&;hMl&zDN_KlN0ZIho;KJDVYmz-Z8@p z2432vITc((AVyjr?X!?9wfvR5!O73c#EfD`s)Jy8PH7Ma^Y5+umez^?{Od72zh27h?5rKxAkV45G52y0#aM5{^N}|4skHJ2 z;}hrFv5FWGZ=Zu{SXa8vP}fccwUrN+igqu~WWV%wSkM1FI4EdW4f!?8+zh+TDD+Fy z4mo8t?Buf@WOR96|8a-Xi~COU1buoDoKJav8K}YGs=&0sgr5$C3x@nsG%ez~Uz}ZA z<&SW#*xA`VQPzo`E2(jdADm*>-xAaAk0GV%VoHhz_p zFEFGSg?%lCR|v-N8(+Cr&AVLo$S7k2h20bje4L$K&nkazS#9NInmuhI8JY~fH0|IV z1OszuG+JrqwsOWG!)|WIvC^|8K?{~%e52fJ`CAWN|H}nw^=pofj&L}3COc8tI|1os zjn|oM(LTu={0MvG-~Wm+IOt1g6m#hb2?Z~&hL-^ADXtt z?AqE|%;G2~lJ@Equ#}S&Ziu`6@C}LTD0Vud1W|+LpOp1IJ(6*L`xAUi9d?-(E&HI7 zuweg7ywyh^T_V#RUti?scm{mcdiaJCJ5>vIP*oMoV@Cye>14cB*czYd4%Et>g)4Qt zRYS*Rl-qbCA&u;aJ~Kc9SwGMve4JGq(XT%AHtaUUvgBsfZ1qSmbfL0;r=kXB?a`YS z1?b)R&*g?d{h#t+n8>09?%X|Tjn5p4s;a73us~)_7FYD1Xy?9})_t0vtYPJfL@@yH zm2)XAU`+}g!FTThdc90CxRcd>nb>bY@syNJ5hrM zIGmqy2Vx6eOoq7biF#n=SXo&)&n?4MF+Y5`3WtkW5q&-;Ya$}k4Y%;(b}|;&XxN=U zlbfQTVQNHYu~;$R+f-_*suu20j3d5H#*sFOgf!|^1HvVkM2U!a8t}geM`GA0Y0a2W zGkTONE7n7k=LfkZ*5q z7cEbhA?XIS)gasC<>fonF)Z|vO)*WhC&4tPF=Mg9fdc0dhc6wP_~O^ES2md_0uj)= zRy>=i85hOtgkVB2@83(hUB^)ia;Acv>Sm9D=~JhQN@*vsb#|n4{;mmk0|E}7yK^JFucm(rhdXTFg`>=o4a1Id1EdlF%Rd~ciIKb?hNCx7~qY#C*CFYu`* zUh>_WKjfFCe#WCI*}#1K{=QT6d8&6+iwA#}F-P>v1kYlrMF4M-afEO9!sbwH6jQ6n za5QRS7liV(PjXzNIcJ2Z=V#B8y~BDN2H=aC9+}=bM_+81TRNZkG1qzwBvNMp63M3e zbWytqZqb}{9R#)OcKaU%B_%z6Jf-Iz9~S^xkgn+r%&2`$S#fdq+EH!Mr5LCoE7bD( z_3LO+S>rlwCg(|Y(1-y}Fkdqot&Q^poM&Ob5i`wjT+zUB-R2zJ5LkPDRvf8V124ICEepS%i`#pWM&Qper;Yyt zlnixNsklXXijU+gn6G>RVvdY6HDllN_*gGl7cXLypAHR;lTyqCH{ftMR}0A5l}omz zm1rQ%wyfZ*p%)9QtE;V3*tkDkTwKr%&0VJ(DLARv6VE|J6He7mrm|@&wELj9e{uRD z7(TD-w5Cz0$CeKF`e+pQ-XfgO=ckzR-aU79gEv8Q_Sb2>#Mv~2Q26~07C(nBfoPOF z_4XGmbEpx&F+$^nmbZo`n z-=BES{}Tv%WD`C2%l0SSsitj;rmHi#X^hF&y%HU_IwPTn5X4k{PL7vgekfKf=y3xe zry_!D^R8m7&&nuU5yS=9=?*pTBuxu*c-%r2bFc(K*ia*p$$IycO85eS4F5559?+X#d` z&jT=TOr|*;!<_a1^fIE?7(!r4zkuzm0ZHLjEtC~+9Y+TfvD7d z5EQzi!or#M7)>=Fknt2Tmr92|ilOERq>wzjtR##4W?2>yWj-gRdW7qG|SjYlW0f?9@q z)em+8tbJ+UhAY^I@S}TEYmiZ}7krS9=KKL?UaL6(e&Gq6jN$p;Vdbz zlryOhi-?E-oCDdeiKeqaX+VR$Sx``rkB^VETIhLT_;=5a6crUYfa=OhslSOp031_W zE`bDG%7YQO??dE3kmcD_M+@d_fn@-lk#DE$(fN3M$SyhiH}A+5)cJ=H*K928%BU_; wfPckcprS;0rlzdjj}zLD6WZTTXzMCz9l^bEQ06-Lu^?q*Wq+ykqDTC{0nHq)$ diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png index 7caf5cb742a27f001106cc3ade926f2f177e71fb..cb640a7409fccce7b08ec23bdf728f11a78a1533 100644 GIT binary patch literal 4154 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 literal 4170 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 T-aH8YY>?KX<8lAr0RHTE diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png index aadb5191f0e7df6ae1838ca6b04745268de17728..f539a8e62e66d1769e1bd692594d5938db7d1d46 100644 GIT binary patch literal 4189 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 AyZ`_I literal 4216 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 Ha8LLb^xtTU literal 4247 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 sq)Epb{z}gH3sL0nTmDag9FofH^0IWV!Z diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png index 7568dd63a33181ae5ec5c8c99ac72c18859109b6..4cce8f6a00ead59f2f061bde6a6b374e9fecbfe3 100644 GIT binary patch literal 4149 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

Gc&a5H1zmVybWfk{$63_yW#%@t&MX| diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png index b8c3b5b143d2b7b67116daaef4f7b3f119777494..93d8162b3bf9196b44bc38721cbc254de2854cb4 100644 GIT binary patch literal 4144 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 literal 4149 zcmeI0c{JPU8pn0lO|(^=*4Bk;OLba9izaGXOVLmLD@yD^+M21BmX;A>Z7FJL z7`qTlwG^?G5)l%+Mj|RANJ8#w{<`PfKkoc-|GH;#PV%1fJHPY3@AEw0@8@|=o}0q; z_wPBjM?^$q|4oA%<{~0nUTuCww}K~-(YI;fS8(%&jzviN{HWIhiNMCD#Zk$35;{}I zOvBGPH*t2{cygffQM_TcUXIyy(PyoqdarNj9*RqrFpxNW$mK;$vs6s$5krZBC&_0* zw*2zneTwp} z;I@*IQW{Dk-9081B_g74So-Ma?R!P9|Fis)f`3;*!t(NR7xd{^LOTa72bVW*!^a74 z%Ev6JWnx;4V#5y=rz<4VgAqkmALi!f;=E-lD=QTg6hdXJko*;o$5+{}#sAtZT%L&S zfxTo`n2K=!Q({G9_yBr>fFxL3N<&={PjTLIS5#8`>Hm6>XD}FPsC~@}iMxC5UeA== z(O5|zGM0x8rwekEI*D zkcDc#Hh5=VV3wm7v9UJ0<{*qYtb}Xa;BDI_CV%j`olotwV5z5Pfpj~A7}9j#;o=7$3}1Zy&y0S591`P})srEaZV5ZSB2tPzWIfe)07bImO+Esr>c zjCWn8rQ5*`g0d>pHkJlaQL}GPNbD>i6a45JCmw}tGUGE%1XXZmDC0wQ zcZOE=xVc-ppPr2k*&{z=u+ocEjKL2SaH!dbQkS;4>FMchJNJDb{9xy(H>51K zTyaf>d3T!muwIYqpo-F(0q!8G#baS7$Sv6-+m+m?*uSI;_Y(Oiu6%r7^Va{qXv%tedJb24rx~Aa zdvUaX#HBsz&;z9CjncSih_>J%$WRwFre332J^I>qMo5Y_IuZ#vkp(a9Iw9VCt_Dzg zg^DVv)qj0iacD+7*7@*AN0L&tH)U`n=Sv#c=$UTm9xqU%azvLW%WSG^ca_Gc+a_^h zi_$DAfxp-$r#)ZMA2(HP>fkW?=~S^(W8`p!N3y;D&7MrCk)a`6ll~6G3z1T1ZtxeJ z2w}@_S!!CEU5cDo{^?yOz4PuB)+P^qMzJ#@LvD0b_Q!VVjk1>!;jpY^^Wcg?gJ z#722h#@Lm`Tgr6K{DI*OO0!F_QK4I}j3BSv z{rDTP92&;?e#*u*Ffb6DqPj9SP-JasIiS-CfN_}`l5;&;4Es53h2>ypM|tQCH13AL zCL|;*GzuHWFC{w!*&wj62%cb>dL zX@0#|Qu&OE3Y^C7H_?W!bNcs_)FXHtHk-}iPzfksJUtJO$1`^TEd0JgLu*4F?Ck|} zMJ1Wx?Do(R=`Tl`J^dormr1i(XqYOW$0!M2pst(lg&e(fU$DA}CF@05nwwt&qDX{m zo`u$dWVgwpl$ZJeC`KYyMzQ4fL`CqjeDdP;aZ#(-r9PtzuG~j@FMjf)hR&)H!h=d9 z0Uy=827j?fj5R&(&%4!0gkvwc&JS18>3)-~^HhQs6zbmlrlX_7ekWvy!kzPQfH4FQ zTt%D727dkmOGwf<8TkseK&Z~nJ_#kDg&R`7^@~l9w*lST);zq+YXwE_#;WEAG6vgV z0cU=0_O#&wVc+{4Lu6{qrXz>yQD?V3LwimgQOYdIi9$3_g+NluV1ibpHwi6IStdd<;R+gU`c21Q)g6wgH z?Th>A;z@p%@Qv%iV$3wj3EkzlAF$bLA|LR`J7mTpAD-bDM$gUb$&bcdE49S{?r62C zeUoDabYWH9Fc=JicFCpn1rw6gd#}=ywdIFUI^d~=tE+2~mHvv>v7eo<_3y0**di5)o}$Q%)5L_g~t*-KMS!jkzZ96#QFXe(9}d$PK*r)0o{gcOf$=Sx`~MHIpQdL zy0hFQ2>tzSbVp~WTUTn}TtR-GuT8E0bQA=ZQW72Yt9bE~PM9Z^qp{1w+TKqW!5a;x zjn;=ESuXDj3XZBGh>y4J>MI8pApj2_e0~EssJw@mdeNmdTQ@e>SqVIGop6v$4P$&@ zMx*8{N&chK7tZf#Qb<4!4k;u+Eh7I-WPejJgPcVNpdtY$@1?wTV!1Vn|z@f7lGX1J0%;O7N*vv zEXp(^Yru2>I_Kp@yVV~p5Gkq`uyzEW3oWG0=%DWGiijRd^KBr1IxYlMXKMEz)eQLl zw&Bl-XX={`<>5X?$agRz06cyj%&)HxU!EICN=nMm4Af+P1e8AF6z<3M8E$Eb3C^OesV7Me72$whBoHszj0<#dPYN3o)ki_A` zfq-e+!Zm>X&!3(jN*i)30H4A_-dGf*VQnT2(0>y{Nrp)nOwjkoIay+ z2x?$pz|z9vKK#%oUrEI_NO{`%D?TGN##LZEB!Ec`Py%#<0Q$tK6QPQCG?12<>x+FS zX@W&$cl~)O)xbH6BhiWr(k*{uV)*BkzcW|;e`IOZX({(X@2ZCIIruw30Rrtv+iBzp7ovm?Y+P6+~+qfjRgfn z1$cOP1aFua-s0go^y=U~!VBIAPrk|I;W;67!|>Ye@T`@|2;Vbw^81ZRX{gSbg;OHc zB5E$8*3wefWJKP+nifeAeJksA+qL$VAwsd^TL|3>|mA5jvj_yjfVAJsHa}GMZvA&!}H@477z)+%UZG!{2?BeS=D2;!^e&od>g(&@OKuZ6yGi>YjLrE@w`BI9MDXWp<>loL_VpMH21bZt={0>7l8Bg3J@K#A;@JZO11Z#( zC={wESiX;6O1_2n!wIEJ$NxSwxsO&kcXDfj7P@k##9w4N9dbT$_C)Oedk`Naiv8?s z-{8MOv6zOnDIP>2oo;n?H!m;mhYWKDg}78V;;7hWjbk&$kt~LV+-q4`SxLnDHL0AK zT&cVB>JMkzN=ix$0T{!fB=>au&eWq_2W}xcDzZ~Hy|%XYhf~k(YCN(dsl~evP@`X} zM&sfxMi!NoDJ7fulAGCOHGPrHnYi(>u_nUUZH2mE%0O3Fm*053?5XDu=h}PGPy(wo zzwziLm!WRvA7{3FIP>9oA%{c0DZ^qQgJ;XC&BkH#JpI)n__^!D~X zH=O+>lZ2oT*$PAV*Y*9ij5-63k{UKQH$i}F`&<3onK;Q)&kd1Cw$V9R_8CzeBB}3p4faxA` zXHxsYKpuo}6Bn1bYBoGLxV^1)<_#5v!Wbc)z{0m+fN|`-mGOo!@0M0!C=Oh?xwFq? z62Aui{6$i?sa3J;19Jpt&g8JxXjE$R9o)ydB`^o$T5E@`PEe`T3Z>mjXz={>yYK&- z#sA}}jPFfxad9-4)9Dt}{_+9@uN-mH4Qp^ICgc)scQF$ixKZrjnqX(V5wF$H)gBqmK{qY59)6dQ{V|vy|1tlu7)_rTcefE>;f~ zJxQ7bm{4Y`w*pe|TLPt*fC{qN`=ty3v4Vm^4w6BdkQCqo@}PqUZUo@zcUY>BjkeP= z#);-u+M1dU5esQLcMdc(Ha6C?Jo_CAVdA)E5JV3c1_jQ8(-fh^(korDT3%B?X`7vkevzm zZ{G`c9T$T{2E*ZSR$XrnL{FmtP@Ig|vS3{y7FzYcyrALHBN@G4-QYLgKl#wqMCvEm z<%HG7xx}7)z7C1733)Ukh(YpPyCRoi?ifkQuf#L%p|2tbl64k-1D`O!aMQm&9>pg_SH#_BFNczxrFqoVZwMQ`!()l3?%eO0i7vI+p<>K5rQ+k$b z`mgG_v^~QMUR_AFuD~p!dVJ$rs3VnRjeh+tG_K7W`AxD#Eo(j!%BoSy8SoIoBO-vInNL9`Ru&a~ijr5{N@QO2$F9@^7v0{5MYI3f zPi?J|BaTTQfmuOE3DI27oQQ$|aoy;w(r5vVGqLsocufr_&i?LPdjf4XwDpkGU3_;gtBwEe+dR%(S z+RS(Wp;swoE~Xfydzm(QPWfJ{&V?*3zk7^I?w?_&%BB{8Y*rUVL)WIV=Snp^`+TDp z246`T{I24FGMB^d4&L;N8Nx14U}`dbO!9AG_?7NA2EKoMh?Jzi{+^E{pZ3`YGk^~x z%?T6(UHm3Qo!BYL&6VDHe9kdkip9GJ2nt-dw4$lRx}O(vy(%&t!YNu9^QnYTi{5TF za(XkhJz>loP%l`_xg=QFWJm-;`tsqzCwQ~PwFF`mn?9mnfvc+v0k)J;)92h8>)sqp zD{t^1z=iRzo#-m^S%KFX%N>%@SsiKQM{9V=D5i7d-t=$6Q9$eJ(Hq2xiTgG-bdblT z4a%jp!=+(5%T=wG`WtOMzKbP&tE_FJRgG#WVJYWPS9f>1lASs*lWiF~ogOw7;n|!c zByg=tK!xS7H8CQd=T0_S175-bEW#25Ei5c-Y;2U3l_{iy1oyfn>QalA6>BbrPycFD zdS}7W#xsn6)8yduKK@KVswJjR1x5}Y56bf}?tXtnw{inwXEKaJ0mUYm^v*q_u&AV@ z>1%HM0MXCS40q9<9 zYe3F#pSd9@D2Pe-_w}8a^N#(8bpc}gi?M`|zn`B2K9auDK)wo`00^@VX2Y0#&~f)fGh%nD-VYkqA-I3}iJLd<+%H6tPB&@V`vaMVgoI>}fL7;Yj;WjnzF}=m z94@yffI0`vjr&rg?~`K-AmCN^uGAj#UOq60hp*+WE?#kI0|gJ1GYn{R=(Lea2dozS zly<-rP6xi|rW(}p-8md{Hl9KOd?*JEQ$W}V>WXXo=0Z1-7$*@r3dCt@W~L{Ofnu#~ zZAU)p4ef7zZh>wGUx^=t3NObBip2^^pysgaiIzV1hHB*d&R{s>_GhoF$F`(*<4SiZ2pyhOocc4Y)655W5Tyym6y5 z!!sv?t?oxGupproK~yh$GHDW``r(0$Y)eq&-vyYX%6+hWlS`>pYP!0WB>bZtYb%J2 zy*<{W7e$U@mFMNTwLLqTpPvt)Xx?ZSgX26Nczc(Wmm}vAw41@c4>%6Y2XKV%`{U<- z^tuTmJSv|B2AvS<;Bo{0p1r-iRZbSheESdAL_1KvIf(k{AzN5>=N)o067*oL2e$Q- z`p&BU-q*L5M?3Xe6plA|Q-rv3q+^9oo+g{JySbV6)=w8Y*mk1niHBiW|Jnl=U%B}mdm7>xnkv#8&-#Vo>8#U3anMWGyjMeFD?1{Rl?=E^R}jq zyG12GC}_xAhFlosETy2({NkUFFgOhK;E=jNi)5_B&;1eQk2I!c*ycO#YpWU|`MWqf zBe8mnLMSfwb-Z(G+V9#&L-%%W9D=Whm^bHKSbN&8EtR}_6S~1FI?_ayMzyksTY!H?R z1j3K9n<%cV*0k%bl*aLce_tp*Up_Xq3#%fRKXVi%tn5pFIV*Nxuf_j+S)b*IN{zNA ze@~6sD@clr+mZcb`K^}3^z?Ke?Gb_tQT0le>}Tci*p=?_x)`;LcJKvvJccMGdEvd3 z#(mt`n{WR(sw16X>Fz$dI@iyNn#mQwh0T)YCM#V!?rv_<3GepEdED*NqD>DS%e-9tjyWi-W@?adhj0|Pbb zgohGFciPic`Rpo~V!YkeuPzrO-V_%v<|4-pr=B>~Z>^77p12BwVR&<6e>ECb@lZ%VfBIm7Z_@}MQ(@)!`Gk|hs%1*g@U#;l>-Lo#GkY?FC)0} ziC498rzpFr_a<7Cp!Ja;9^-LDn{xb{YgE*x zsQ9s7&Mqs=?Ck!Y()|;#72Qucq4h-s0)LUzGg#uuUmJpjGAia$%(pitQq0}l+<4q2 zRs%+O;o*153VMNEPqijmlL^Jew!$VYET;SU3mGxO+3mFk1j)arz1F4sr>aI&W18YG3;7IY;akkhm!`-9vJ^(!OT3{aHKml^g7X0egH z6K>*)ksn{$WWcLG#bv6yaDv~k6&J>1mY7pdzgJNBPU6^8SD+;OAK!7#&*Y!_rAH<~ z!jo2AR#wJj>+0&-;>}EPw$nF|jEVpjit9-w;1PqaxE9{pLX}^4_E@vo=46IWr_$X< z(eL$xH&K1Q0Z_F6Q@OFB3h=;;A*M;VqtVNOmNTW}t5;jbxTy>Tx>(-|hEB9j18 zG&HRPT$$>`78Z7PcJ`c`_yd9k-4;ev)$%!1rvlr6z`%|%?da9n#Kgp#&2jmTMR9R) z`B$(7&|`SuzTgpU#QX;k={}j0KA}Dz8-GnP+QF6<;s!)a*RQKC0ulNM|G`CpX6(>MYi~ z_3n2DgTc-MT!@**`F{sY?;8B!)z$9qZeuvRL!H>Av11u%{l+btZ-Ff?-PLb(_I(5J zG`*FaL!D4ne)LiGrbBr1Ap;+K$T?6Um*N{~TO3C>ZJfzMF)2&KwGXgu^$ZNNwYFQ* zq;z6%x(nxY{d!j3I0dl)AU4EcWY;ySq?mjKPkUJyvo^o>PR3!ejyoPT25FOPn!ktM z_!J+ngg&Rk|KQ|s2x1iOe?SSf`d-d6{HhAg1BKhv&;9}DX-?I(3&p?G5?e0qGT zLAX6nAkw?&*?J4JFH$%i8_(~G5okq4MeQcLvrVGDzDtk^gW@5@K!^oQ83;f#vfs|Y z*N;FTxV&-8%&Y(M_CQteNP3>4IXYGiI&@ID7X%X+6r|JdpBFW)iG@O;J!m!cmB$+q zB~%UtbZ~VQkJ*mm)Xb18Dl~4CegsUjV+~h@vefXg<;bP+TqPsfr`~HC?p5%uFU^uV zBx?xbgQknk$&8VuUZN+J%xt-Q`LZHLux!hiUs!-P2)+WG>604AYQkubA*1d0n$0hk zMrTz94f!>wn4tw55e}*>Iu?e;xvJ9kIpyc)169EMBDMvaIJt)zY7X@66{S_%2LmJl zc%J9jTfp=CV=;8YnZ?NI58M2u9Rc%Wqi6St5&So|xGZY*qigj3&oT+`YNJ+wfv~7f z2y!fK>rd%u4Sxz(V>DOU+1a_0rU>C5&BS77ygqKu;Csw|HA}ms;Z`v#Sm{~neQ~}@ zsyNS=Dp~0fyWAoU3IAEKssYZVEXuI@LHU$ozcgKe*6F55itT8pCw;T_9s|(%2`#yb zOwL2CkKkN#ABs!T#}MpFkY|{rX$GhnP(bARqPS9QDlM56z3>J!7#$r=tXl`~tF-YO z0dRPp*6miHvrJn2!&(^-0f2$&=igPdNjQ=b72c9~ctnlLy5D)m*Vp&J5e@lscfEO8 zJRa}+&{{2+r1+!bH}+t+N}@Pn5`6{sx!E}MTt0S?2xF~j0ep4?aOUT$n!3BX;w#W;`8$0K|#hd&%)4KYeUC!=4UzQ7580lN>(&A(%AySF7~Q3 z;(m>XMBG?EoEa1pM5N5WKE{&Gem^)kI6PcE!QPPx=$1AK$wDE)iD!M6BtTWFvPED2 z!Vs}xGX2VZlgrjf@vlYHhE&2OU&wHmQ%(h#qI$ZzlU~%avU^n0(UW%B6~QCr5pwX5 z5Kq7zFs94_91O#zgON)B@6Wc_y8Q)qK}eMzQ?AE)^BIroyZ*OFyP3;-(6Se`D+w|$ z=Kk=}PD6O77;~xtvAPxkz)|x>g^v~VUKbSw0P^EOZ`oij2$pI7p&wtQm}Tn*I8Kw$ z0r85_cdV>hdY@bHxl2O+QbXu?Z4?-;>MkW+vFN)yL(8wb!)proANA>xTwgzshgW6b z0KMKb2;gLAv^XqLm+J9o-kfs$u|7^Zm1GP~eYt{y>;@&HjU0}qUmB19t0&mz9~U0p z7sz@696_qe0ta4$CWOxx)-ZL7*(J&wU`S8Pe+3my-xAI3=;#17j=+xPGM5h~UHqar zXnqsSXK>_%ym4@K9Vx`4-GCW{z(@B#f#aHt{W)cR00VHXoT#o9Hr>^gv%ppWGFvyu z9UK_&#b^bP>1>JcTRed|N$Ye5li+4!=>^~|hR)MpL#lwzI5Z(hA> zdv-GVOOrU!qvk%cig6?ZKZaRGsHqdu?+o4>> zOZv!9r`MoD11g2GLTI+%dqgVETlD6)?(c8bFQ diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png index 49468698cd4e749ad6a666bb253a45695a3e6839..d2270826a5bd84efa4a8dbde8a987d5db0a2dc94 100644 GIT binary patch delta 2732 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 diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png index 50bdac3d8f39aa492bf41ba3e49c787031d07628..2287071ffab678232370853706c56c3c843d4fbb 100644 GIT binary patch delta 684 zcmV;d0#p5`2B`*+Bs=g)L_t(|obB6Na_b-vMNuV{DP{~FQ^w#yDj!NwB!W&O9tY&T z>VEm!h_-ZqAo*?I_YHu~ySf9wtCQ~nB9VO!f9yluffk;R#EYL_vUV`iu(K8)sI>j( z!}tg@A<)^8-sE7hZClw)_@|X*vOVua~gKmjstaQpP3L4 zE13yDzkY@RV~mVPi&ciwTk@ZX^Gw2t8;h9`5F>8du4`%9CAz1ya4BB4 z@Ux?<#4qL?U*=SFXaDkhT79Y^{}Q^jOCby^gxo(*3-?nxoDW!UCl3*N=(iE2&skcZ(Ml1*+mF8f{l?|oue1q93h7MfAAVLjug*tCR6k-c9tUy$ zTzi|fv>MJ?q2@UU^BNX~Fg~NkQ?s3)+3UhCEq-yKq7FAM?U1_~Yp%oO!?C};?fYKb z)*;!U(w~8NMi--n(!nX$RI7IUK@Cd&6#Tb*Pp|4rbAH>c+G*9`dBVK#b)`O{{ym|w z5?FLJJ~(lOnCqK z8wN6CWH?%^GL&AD{X`sR5>DK3%!Gg#Aq2$MzeRykes_VD=N!}_yzt$*5N1yK>RUTAmsYvS-n+D8`@{B%Ql<6pfGC9d=MVh-%@uQ)Db4@a zB&C&hqg84C4Pk@PMe?vKA+tn(K6($GijA4D;Amm~_2urE=SvG;6qc6y+Vi@Wmdb!@ zR+zxli*|;Om(PSv)-#sE+?~luCVveJhY1$>ar8Ivjp0@C_qpQR( zWE@}SRCH(m^m|%;sv-Llx`Xl%`V~UzpQVNCDILxSthb|w@IAD5pm%BVZ7O32y7g1B zF;jZwEwmy9%FVqTMML?qZ@E3N3sKC3Ol$Zk?=E=hEeZ^FyaBLrI`mkMHh)rjK@Z-S zuisvuwzPW2(z=aS!$Wxa(bvA;xSZ>iHpfVzn+fg1&noBD_{fOrM+}+AL5x4w-exVW zhH+M?dCuY8HLM6>ct$f%&Gvt0uf2R};fo6u> 24) & 0xFF) - x, y = (int(c) for c in coord) + x, y = coord self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) else: self.draw.draw_bitmap(coord, mask, ink) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 457e906c872..c8de65be2d2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,6 +26,7 @@ # import base64 +import math import os import sys import warnings @@ -588,6 +589,7 @@ def getmask( stroke_width=0, anchor=None, ink=0, + start=None, ): """ Create a bitmap for the text. @@ -659,6 +661,7 @@ def getmask( stroke_width=stroke_width, anchor=anchor, ink=ink, + start=start, )[0] def getmask2( @@ -672,6 +675,7 @@ def getmask2( stroke_width=0, anchor=None, ink=0, + start=None, *args, **kwargs, ): @@ -750,12 +754,23 @@ def getmask2( size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) - size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 + if start is None: + start = (0, 0) + size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) offset = offset[0] - stroke_width, offset[1] - stroke_width Image._decompression_bomb_check(size) im = fill("RGBA" if mode == "RGBA" else "L", size, 0) self.font.render( - text, im.id, mode, direction, features, language, stroke_width, ink + text, + im.id, + mode, + direction, + features, + language, + stroke_width, + ink, + start[0], + start[1], ) return im, offset diff --git a/src/_imagingft.c b/src/_imagingft.c index bd409917601..b52d6353ebc 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -777,13 +777,15 @@ font_render(FontObject *self, PyObject *args) { const char *lang = NULL; PyObject *features = Py_None; PyObject *string; + float x_start = 0; + float y_start = 0; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziL:render", + "On|zzOziLff:render", &string, &id, &mode, @@ -791,7 +793,9 @@ font_render(FontObject *self, PyObject *args) { &features, &lang, &stroke_width, - &foreground_ink_long)) { + &foreground_ink_long, + &x_start, + &y_start)) { return NULL; } @@ -876,8 +880,8 @@ font_render(FontObject *self, PyObject *args) { } /* set pen position to text origin */ - x = (-x_min + stroke_width) << 6; - y = (-y_max + (-stroke_width)) << 6; + x = (-x_min + stroke_width + x_start) * 64; + y = (-y_max + (-stroke_width) - y_start) * 64; if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; From 97a6f651d4ddf07cbd13b2cd38b19dde381050c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 12:01:15 +1100 Subject: [PATCH 026/727] Added Interop tags --- docs/reference/ExifTags.rst | 50 +++++++++++++++++++++---------------- src/PIL/ExifTags.py | 8 ++++++ 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index ff57885240e..d362334a59c 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,8 +4,35 @@ :py:mod:`~PIL.ExifTags` Module ============================== -The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which -provide constants and clear-text names for various well-known EXIF tags. +The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes +which provide constants and clear-text names for various well-known EXIF tags. + +.. py:data:: Base + + >>> from PIL.ExifTags import Base + >>> Base.ImageDescription.value + 270 + >>> Base(270).name + 'ImageDescription' + +.. py:data:: GPS + + >>> from PIL.ExifTags import GPS + >>> GPS.GPSDestLatitude.value + 20 + >>> GPS(20).name + 'GPSDestLatitude' + +.. py:data:: Interop + + >>> from PIL.ExifTags import Interop + >>> Interop.RelatedImageFileFormat.value + 4096 + >>> Interop(4096).name + 'RelatedImageFileFormat' + + +Two of these values are also exposed as dictionaries. .. py:data:: TAGS :type: dict @@ -26,22 +53,3 @@ provide constants and clear-text names for various well-known EXIF tags. >>> from PIL.ExifTags import GPSTAGS >>> GPSTAGS[20] 'GPSDestLatitude' - - -These values are also exposed as ``enum.IntEnum`` classes. - -.. py:data:: Base - - >>> from PIL.ExifTags import Base - >>> Base.ImageDescription.value - 270 - >>> Base(270).name - 'ImageDescription' - -.. py:data:: GPS - - >>> from PIL.ExifTags import GPS - >>> GPS.GPSDestLatitude.value - 20 - >>> GPS(20).name - 'GPSDestLatitude' diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index f3a73bf1a52..c00730ba91d 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -338,3 +338,11 @@ class GPS(IntEnum): """Maps EXIF GPS tags to tag names.""" GPSTAGS = {i.value: i.name for i in GPS} + + +class Interop(IntEnum): + InteropIndex = 1 + InteropVersion = 2 + RelatedImageFileFormat = 4096 + RelatedImageWidth = 4097 + RleatedImageHeight = 4098 From ebde03eae829ca8396dbb446157daf5a5b04e67c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 13:10:08 +1100 Subject: [PATCH 027/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fc8d8362a2a..574fbdbd368 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- CVE-2007-4559 patch when building on Windows #6704 + [TrellixVulnTeam, nulano, radarhere] + - Fix compiler warning: accessing 64 bytes in a region of size 48 #6714 [wiredfool] From 73bec9622413cf52b71f23723aad6ce8b01c4445 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Nov 2022 21:50:06 +1100 Subject: [PATCH 028/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 574fbdbd368..34c00c3d462 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added Interop to ExifTags #6724 + [radarhere] + - CVE-2007-4559 patch when building on Windows #6704 [TrellixVulnTeam, nulano, radarhere] From 62fd8336b93e97c15ed1e9afee31ccc1d9b15362 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Nov 2022 18:42:46 +1100 Subject: [PATCH 029/727] Update to Python 3.11 in GitHub Actions --- .github/workflows/lint.yml | 4 ++-- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6195f973b05..8a14dad92e6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.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 @@ -30,7 +30,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" cache: pip cache-dependency-path: "setup.py" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6b7f62c237e..5cabb662212 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -226,7 +226,7 @@ jobs: path: dist\*.whl - name: Upload fribidi.dll - if: "github.event_name != 'pull_request' && matrix.python-version == 3.10" + if: "github.event_name != 'pull_request' && matrix.python-version == 3.11" uses: actions/upload-artifact@v3 with: name: fribidi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 645384c02d8..831e33c130d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.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 @@ -96,7 +96,7 @@ jobs: path: Tests/errors - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11 run: | make doccheck From b0ab324f829f8016f89141b332998ba67e3867c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 9 Nov 2022 20:03:16 +1100 Subject: [PATCH 030/727] Use the latest Python version Co-authored-by: Hugo van Kemenade --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8a14dad92e6..49611e2879e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,7 +30,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.x" cache: pip cache-dependency-path: "setup.py" From 1c032ff5db5854895314045ee25950a06bea2ae8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Nov 2022 10:37:22 +1100 Subject: [PATCH 031/727] Revert "Install NumPy with OpenBLAS" This reverts commit c82483e35a37919df9700485aa752e8c5a38f28c. --- .github/workflows/macos-install.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 65f2b81d543..dfd7d055302 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,7 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas libraqm +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm PYTHONOPTIMIZE=0 python3 -m pip install cffi python3 -m pip install coverage @@ -13,7 +13,6 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg python3 -m pip install numpy # extra test images From 99a11297b108c1427cef683d9dcd196f32794043 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Nov 2022 16:08:42 +1100 Subject: [PATCH 032/727] Updated macOS tested Pillow versions [ci skip] --- docs/installation.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c6509564095..f4e959fbe57 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -480,11 +480,13 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |arm | +| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.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 | 9.2.0 |x86-64 | +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 | | +---------------------------+------------------+ | | | 3.6 | 8.4.0 | | +----------------------------------+---------------------------+------------------+--------------+ From 9fbfd3f00efbb6719bb35043315e457614617072 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Nov 2022 21:32:40 +1100 Subject: [PATCH 033/727] Added oss-fuzz badge --- docs/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 45af4c5714c..1efbe74c4cb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,6 +61,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Sun, 13 Nov 2022 08:00:20 +1100 Subject: [PATCH 034/727] Added MP Format Version when saving --- Tests/test_file_mpo.py | 1 + src/PIL/MpoImagePlugin.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index d94bdaa96c9..dba1ec1b11c 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -268,6 +268,7 @@ def test_save_all(): im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) assert_image_equal(im, im_reloaded) + assert im_reloaded.mpinfo[45056] == b"0100" im_reloaded.seek(1) assert_image_similar(im2, im_reloaded, 1) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 5bfd8efc1a6..92d288f2fb1 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -51,7 +51,7 @@ def _save_all(im, fp, filename): if not offsets: # APP2 marker im.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70 + b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) @@ -60,6 +60,7 @@ def _save_all(im, fp, filename): offsets.append(fp.tell() - offsets[-1]) ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB000] = b"0100" ifd[0xB001] = len(offsets) mpentries = b"" From 20f17cc6a79881ba441adfb734e8dbe6901a9749 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:14:37 -0600 Subject: [PATCH 035/727] remove unused ImagingAccess->line() method defs --- src/libImaging/Access.c | 62 +++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 514fb292913..83860c38a7e 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -43,23 +43,6 @@ add_item(const char *mode) { return &access_table[i]; } -/* fetch pointer to pixel line */ - -static void * -line_8(Imaging im, int x, int y) { - return &im->image8[y][x]; -} - -static void * -line_16(Imaging im, int x, int y) { - return &im->image8[y][x + x]; -} - -static void * -line_32(Imaging im, int x, int y) { - return &im->image32[y][x]; -} - /* fetch individual pixel */ static void @@ -187,36 +170,35 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { void ImagingAccessInit() { -#define ADD(mode_, line_, get_pixel_, put_pixel_) \ +#define ADD(mode_, get_pixel_, put_pixel_) \ { \ ImagingAccess access = add_item(mode_); \ - access->line = line_; \ access->get_pixel = get_pixel_; \ access->put_pixel = put_pixel_; \ } /* populate access table */ - ADD("1", line_8, get_pixel_8, put_pixel_8); - ADD("L", line_8, get_pixel_8, put_pixel_8); - ADD("LA", line_32, get_pixel, put_pixel); - ADD("La", line_32, get_pixel, put_pixel); - ADD("I", line_32, get_pixel_32, put_pixel_32); - ADD("I;16", line_16, get_pixel_16L, put_pixel_16L); - ADD("I;16L", line_16, get_pixel_16L, put_pixel_16L); - ADD("I;16B", line_16, get_pixel_16B, put_pixel_16B); - ADD("I;32L", line_32, get_pixel_32L, put_pixel_32L); - ADD("I;32B", line_32, get_pixel_32B, put_pixel_32B); - ADD("F", line_32, get_pixel_32, put_pixel_32); - ADD("P", line_8, get_pixel_8, put_pixel_8); - ADD("PA", line_32, get_pixel, put_pixel); - ADD("RGB", line_32, get_pixel_32, put_pixel_32); - ADD("RGBA", line_32, get_pixel_32, put_pixel_32); - ADD("RGBa", line_32, get_pixel_32, put_pixel_32); - ADD("RGBX", line_32, get_pixel_32, put_pixel_32); - ADD("CMYK", line_32, get_pixel_32, put_pixel_32); - ADD("YCbCr", line_32, get_pixel_32, put_pixel_32); - ADD("LAB", line_32, get_pixel_32, put_pixel_32); - ADD("HSV", line_32, get_pixel_32, put_pixel_32); + ADD("1", get_pixel_8, put_pixel_8); + ADD("L", get_pixel_8, put_pixel_8); + ADD("LA", get_pixel, put_pixel); + ADD("La", get_pixel, put_pixel); + ADD("I", get_pixel_32, put_pixel_32); + ADD("I;16", get_pixel_16L, put_pixel_16L); + ADD("I;16L", get_pixel_16L, put_pixel_16L); + ADD("I;16B", get_pixel_16B, put_pixel_16B); + ADD("I;32L", get_pixel_32L, put_pixel_32L); + ADD("I;32B", get_pixel_32B, put_pixel_32B); + ADD("F", get_pixel_32, put_pixel_32); + ADD("P", get_pixel_8, put_pixel_8); + ADD("PA", get_pixel, put_pixel); + ADD("RGB", get_pixel_32, put_pixel_32); + ADD("RGBA", get_pixel_32, put_pixel_32); + ADD("RGBa", get_pixel_32, put_pixel_32); + ADD("RGBX", get_pixel_32, put_pixel_32); + ADD("CMYK", get_pixel_32, put_pixel_32); + ADD("YCbCr", get_pixel_32, put_pixel_32); + ADD("LAB", get_pixel_32, put_pixel_32); + ADD("HSV", get_pixel_32, put_pixel_32); } ImagingAccess From 16994ccc9b40f97113cb1c8f56abe9a37a2744fa Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:15:50 -0600 Subject: [PATCH 036/727] remove unused ImagingAccess->line() method def --- src/libImaging/Imaging.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index b65f8eadd51..d9ded185238 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -124,7 +124,6 @@ struct ImagingMemoryInstance { struct ImagingAccessInstance { const char *mode; - void *(*line)(Imaging im, int x, int y); void (*get_pixel)(Imaging im, int x, int y, void *pixel); void (*put_pixel)(Imaging im, int x, int y, const void *pixel); }; From 55abf18f1020b456cd782ebfe94b5847148559f6 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 12 Nov 2022 17:16:50 -0600 Subject: [PATCH 037/727] remove comment about Access.c line methods --- src/PIL/PyAccess.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 9a2ec48fc60..039f5ceeacf 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -13,8 +13,7 @@ # Notes: # -# * Implements the pixel access object following Access. -# * Does not implement the line functions, as they don't appear to be used +# * Implements the pixel access object following Access.c # * Taking only the tuple form, which is used from python. # * Fill.c uses the integer form, but it's still going to use the old # Access.c implementation. From 21f202a22a712e0392c4ee148946967bd05eb936 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Nov 2022 06:06:08 +1100 Subject: [PATCH 038/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 34c00c3d462..bf9a236cc5f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added MP Format Version when saving MPO #6735 + [radarhere] + - Added Interop to ExifTags #6724 [radarhere] From 84458c3988ad22d82afaa33a4d66080a2a089908 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Nov 2022 08:18:31 +1100 Subject: [PATCH 039/727] Updated xz to 5.2.8 --- 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 1fcec66b374..10e2000ae68 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ def cmd_msbuild( "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.7.tar.gz/download", - "filename": "xz-5.2.7.tar.gz", - "dir": "xz-5.2.7", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.8.tar.gz/download", + "filename": "xz-5.2.8.tar.gz", + "dir": "xz-5.2.8", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From 8a3ba659450f2b46da7c0e4df92fa10dfc0a21d4 Mon Sep 17 00:00:00 2001 From: Alex Clark Date: Mon, 14 Nov 2022 10:57:30 -0500 Subject: [PATCH 040/727] Remove Tidelift alignment action and badge Not sure if we still care about this? cf. #5762 #5763 --- .github/workflows/tidelift.yml | 36 ---------------------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/workflows/tidelift.yml diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml deleted file mode 100644 index 69f9e547602..00000000000 --- a/.github/workflows/tidelift.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Tidelift Align - -on: - schedule: - - cron: "30 2 * * *" # daily at 02:30 UTC - push: - paths: - - "Pipfile*" - - ".github/workflows/tidelift.yml" - pull_request: - paths: - - "Pipfile*" - - ".github/workflows/tidelift.yml" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - if: github.repository_owner == 'python-pillow' - name: Run Tidelift to ensure approved open source packages are in use - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Scan - uses: tidelift/alignment-action@main - env: - TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }} - TIDELIFT_ORGANIZATION: team/aclark4life - TIDELIFT_PROJECT: pillow From 70cc8a57415f6a9744cf4d4f0407a4415319dda2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Nov 2022 09:06:41 +1100 Subject: [PATCH 041/727] Fixed writing int as BYTE 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 d38c1c523ea..b90dde3d96c 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -201,6 +201,22 @@ def test_writing_bytes_to_ascii(tmp_path): assert reloaded.tag_v2[271] == "test" +def test_writing_int_to_bytes(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[700] + assert tag.type == TiffTags.BYTE + + info[700] = 1 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[700] == b"\x01" + + 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 1dfd5275fa1..ab9ac5ea23a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -719,6 +719,8 @@ def load_byte(self, data, legacy_api=True): @_register_writer(1) # Basic type, except for the legacy API. def write_byte(self, data): + if isinstance(data, int): + data = bytes((data,)) return data @_register_loader(2, 1) From ddc215ce3c6772a9b3aa6995f38c97ea922ecf48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 15 Nov 2022 09:51:20 +1100 Subject: [PATCH 042/727] Revert "Added Tidelift Align badge to docs" This reverts commit 06ab0324a3bb66965c7c1505dbdd0aa640ba308b. --- docs/index.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 1efbe74c4cb..5bcd5afa5b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,10 +57,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Tue, 15 Nov 2022 11:35:14 +1100 Subject: [PATCH 043/727] Revert "Add tidelift alignment badge" This reverts commit c8822a6cac65bbe0a7d831ef2b8431435f1feb1d. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 7a81e0c404d..8ee68f9b848 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,6 @@ As of 2019, Pillow development is Code coverage - Tidelift Align Fuzzing Status From d4c7bd7e19e926d5d47c70d04eaba86cf20a67a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:04:02 +1100 Subject: [PATCH 044/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bf9a236cc5f..87ff33f1fa3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed writing int as BYTE tag #6740 + [radarhere] + - Added MP Format Version when saving MPO #6735 [radarhere] From 70c8e342a514815ae698a3fb22e2ab52f3c09d7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:15:56 +1100 Subject: [PATCH 045/727] Added "start" argument to docstring --- src/PIL/ImageFont.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c8de65be2d2..3b1a2a23ab4 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -649,6 +649,11 @@ def getmask( .. versionadded:: 8.0.0 + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ @@ -743,6 +748,11 @@ def getmask2( .. versionadded:: 8.0.0 + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + :return: A tuple of an internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking From 62db04478733e9baddd41d365f96f9a8dbeb388d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 09:27:33 +1100 Subject: [PATCH 046/727] Added release notes --- docs/releasenotes/9.4.0.rst | 54 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 55 insertions(+) create mode 100644 docs/releasenotes/9.4.0.rst diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst new file mode 100644 index 00000000000..46c7e2f2285 --- /dev/null +++ b/docs/releasenotes/9.4.0.rst @@ -0,0 +1,54 @@ +9.4.0 +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Added start position for getmask and getmask2 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Text may render differently when starting at fractional coordinates, so +:py:meth:`.FreeTypeFont.getmask` and :py:meth:`.FreeTypeFont.getmask2` now +support a ``start`` argument. This tuple of horizontal and vertical offset +will be used internally by :py:meth:`.ImageDraw.text` to more accurately place +text at the ``xy`` coordinates. + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 8c436be3bd8..a2b58869696 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.4.0 9.3.0 9.2.0 9.1.1 From cb40f46ec13f2413163baa00e6e44958dd3f67f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Nov 2022 14:58:21 +1100 Subject: [PATCH 047/727] Added Fedora 37 --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 1e36b338299..7331cf8ee0d 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -31,6 +31,7 @@ jobs: debian-10-buster-x86, debian-11-bullseye-x86, fedora-36-amd64, + fedora-37-amd64, gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index f4e959fbe57..cf6b9ca8fb5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -442,6 +442,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 36 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 37 | 3.11 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | From df8e87291254dc22415a0c6e371950e79311dbf2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Nov 2022 08:26:47 +1100 Subject: [PATCH 048/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 87ff33f1fa3..cd1b07be425 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Use fractional coordinates when drawing text #6722 + [radarhere] + - Fixed writing int as BYTE tag #6740 [radarhere] From 1f6df76c42dab44e00b128b40fd6657185925aca Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Thu, 17 Nov 2022 13:58:07 -0800 Subject: [PATCH 049/727] updated webp with exact parameter. --- Tests/test_file_webp_alpha.py | 34 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 4 ++++ src/PIL/WebPImagePlugin.py | 2 ++ src/_webp.c | 5 +++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index dc82fb742b2..07df7a06856 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -96,6 +96,40 @@ def test_write_rgba(tmp_path): else: assert_image_similar(image, pil_image, 1.0) +def test_write_rgba_keep_transparent(tmp_path): + """ + Can we write a RGBA mode file to WebP while preserving + the transparent RGB without error. + Does it have the bits we expect? + """ + + temp_output_file = str(tmp_path / "temp.webp") + + input_image = hopper("RGB") + # make a copy of the image + output_image = input_image.copy() + # make a single channel image with the same size as input_image + new_alpha = Image.new("L", input_image.size, 255) + # make the left half transparent + new_alpha.paste((0,), (0, 0, new_alpha.size[0]//2, new_alpha.size[1])) + # putalpha on output_image + output_image.putalpha(new_alpha) + + # now save with transparent area preserved. + output_image.save(temp_output_file, "WEBP", exact=True, lossless=True) + # even though it is lossless, if we don't put exact=True, the transparent + # area will be filled with black (or something more conducive to compression) + + with Image.open(temp_output_file) as image: + image.load() + + assert image.mode == "RGBA" + assert image.format == "WEBP" + image.load() + image = image.convert("RGB") + assert_image_similar(image, input_image, 1.0) + + def test_write_unsupported_mode_PA(tmp_path): """ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1e79db68bcb..ffc94914873 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1124,6 +1124,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **method** Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. +**exact** + If true, preserve the transparent RGB values. Otherwise, discard + invisible RGB values for better compression. Defaults to false. + **icc_profile** The ICC Profile to include in the saved file. Only supported if the system WebP library was built with webpmux support. diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 5eaeb10ccd5..c88f730a2a7 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -318,6 +318,7 @@ def _save(im, fp, filename): exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") method = im.encoderinfo.get("method", 4) + exact = im.encoderinfo.get("exact", False) if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -336,6 +337,7 @@ def _save(im, fp, filename): im.mode, icc_profile, method, + 1 if exact else 0, exif, xmp, ) diff --git a/src/_webp.c b/src/_webp.c index fd99116cb41..ec9425d3684 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -576,6 +576,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { int lossless; float quality_factor; int method; + int exact; uint8_t *rgb; uint8_t *icc_bytes; uint8_t *exif_bytes; @@ -597,7 +598,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "y#iiifss#is#s#", + "y#iiifss#iis#s#", (char **)&rgb, &size, &width, @@ -608,6 +609,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { &icc_bytes, &icc_size, &method, + &exact, &exif_bytes, &exif_size, &xmp_bytes, @@ -633,6 +635,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.lossless = lossless; config.quality = quality_factor; config.method = method; + config.exact = exact; // Validate the config if (!WebPValidateConfig(&config)) { From 770560d8e4972d69193a816713be2bda5ff3ed94 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 22:06:14 +0000 Subject: [PATCH 050/727] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_webp_alpha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 07df7a06856..5a57d591add 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -96,6 +96,7 @@ def test_write_rgba(tmp_path): else: assert_image_similar(image, pil_image, 1.0) + def test_write_rgba_keep_transparent(tmp_path): """ Can we write a RGBA mode file to WebP while preserving @@ -111,7 +112,7 @@ def test_write_rgba_keep_transparent(tmp_path): # make a single channel image with the same size as input_image new_alpha = Image.new("L", input_image.size, 255) # make the left half transparent - new_alpha.paste((0,), (0, 0, new_alpha.size[0]//2, new_alpha.size[1])) + new_alpha.paste((0,), (0, 0, new_alpha.size[0] // 2, new_alpha.size[1])) # putalpha on output_image output_image.putalpha(new_alpha) @@ -130,7 +131,6 @@ def test_write_rgba_keep_transparent(tmp_path): assert_image_similar(image, input_image, 1.0) - def test_write_unsupported_mode_PA(tmp_path): """ Saving a palette-based file with transparency to WebP format From 3587f27780a5be7d02d0c781b39e13919c88d8d6 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 10:15:24 -0800 Subject: [PATCH 051/727] Added version check for WebP --- src/_webp.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_webp.c b/src/_webp.c index ec9425d3684..9231150aa55 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -635,7 +635,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.lossless = lossless; config.quality = quality_factor; config.method = method; +#if WEBP_ENCODER_ABI_VERSION >= 0x0209 + // the exact flag is only available in libwebp 0.5.0 and later config.exact = exact; +#endif // Validate the config if (!WebPValidateConfig(&config)) { From fdf074b050f272e60e37bbd7f6ff52f3fc95a299 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 10:22:33 -0800 Subject: [PATCH 052/727] added a note to the docs for webp --- docs/handbook/image-file-formats.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ffc94914873..9c2319b44e3 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1127,6 +1127,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exact** If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false. + Requires LibWebP 0.5.0 or later. **icc_profile** The ICC Profile to include in the saved file. Only supported if From 509dcbf073b3cf8c4fc3d051ebc884a6672e070b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 15:35:06 +1100 Subject: [PATCH 053/727] Added LightSource tag values --- src/PIL/ExifTags.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index c00730ba91d..3df1dbf7264 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -346,3 +346,27 @@ class Interop(IntEnum): RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 RleatedImageHeight = 4098 + + +class LightSource(IntEnum): + Unknown = 0 + Daylight = 1 + Fluorescent = 2 + Tungsten = 3 + Flash = 4 + Fine = 9 + Cloudy = 10 + Shade = 11 + DaylightFluorescent = 12 + DayWhiteFluorescent = 13 + CoolWhiteFluorescent = 14 + WhiteFluorescent = 15 + StandardLightA = 17 + StandardLightB = 18 + StandardLightC = 19 + D55 = 20 + D65 = 21 + D75 = 22 + D50 = 23 + ISO = 24 + Other = 255 From 96a4d98abc265dabc1442a3e7b0cfdd5043f90b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:07:43 +1100 Subject: [PATCH 054/727] Simplified code --- src/PIL/WebPImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index c88f730a2a7..e3c19db3dbf 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -318,7 +318,7 @@ def _save(im, fp, filename): exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") method = im.encoderinfo.get("method", 4) - exact = im.encoderinfo.get("exact", False) + exact = 1 if im.encoderinfo.get("exact") else 0 if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -337,7 +337,7 @@ def _save(im, fp, filename): im.mode, icc_profile, method, - 1 if exact else 0, + exact, exif, xmp, ) From 7e5e843d5cd9f4a17361853138816773da28d8c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:12:51 +1100 Subject: [PATCH 055/727] Note that the fill behaviour only affects libwebp >= 0.5 --- Tests/test_file_webp_alpha.py | 45 ++++++++++++++++------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 5a57d591add..df6cffb1751 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -97,38 +97,33 @@ def test_write_rgba(tmp_path): assert_image_similar(image, pil_image, 1.0) -def test_write_rgba_keep_transparent(tmp_path): +def test_keep_rgb_values_when_transparent(tmp_path): """ - Can we write a RGBA mode file to WebP while preserving - the transparent RGB without error. - Does it have the bits we expect? + Saving transparent pixels should retain their original RGB values + when using the "exact" parameter. """ - temp_output_file = str(tmp_path / "temp.webp") + image = hopper("RGB") - input_image = hopper("RGB") - # make a copy of the image - output_image = input_image.copy() - # make a single channel image with the same size as input_image - new_alpha = Image.new("L", input_image.size, 255) - # make the left half transparent - new_alpha.paste((0,), (0, 0, new_alpha.size[0] // 2, new_alpha.size[1])) - # putalpha on output_image - output_image.putalpha(new_alpha) + # create a copy of the image + # with the left half transparent + half_transparent_image = image.copy() + new_alpha = Image.new("L", (128, 128), 255) + new_alpha.paste(0, (0, 0, 64, 128)) + half_transparent_image.putalpha(new_alpha) - # now save with transparent area preserved. - output_image.save(temp_output_file, "WEBP", exact=True, lossless=True) - # even though it is lossless, if we don't put exact=True, the transparent - # area will be filled with black (or something more conducive to compression) + # save with transparent area preserved + temp_file = str(tmp_path / "temp.webp") + half_transparent_image.save(temp_file, exact=True, lossless=True) - with Image.open(temp_output_file) as image: - image.load() + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "RGBA" + assert reloaded.format == "WEBP" - assert image.mode == "RGBA" - assert image.format == "WEBP" - image.load() - image = image.convert("RGB") - assert_image_similar(image, input_image, 1.0) + # even though it is lossless, if we don't use exact=True + # in libwebp >= 0.5, the transparent area will be filled with black + # (or something more conducive to compression) + assert_image_similar(reloaded.convert("RGB"), image, 1) def test_write_unsupported_mode_PA(tmp_path): From 3c7aa133eb62d75c0f96360ba54c7c4ed19d5c6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Nov 2022 17:18:27 +1100 Subject: [PATCH 056/727] Assert that image is equal --- Tests/test_file_webp_alpha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index df6cffb1751..5970fd2a371 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -123,7 +123,7 @@ def test_keep_rgb_values_when_transparent(tmp_path): # even though it is lossless, if we don't use exact=True # in libwebp >= 0.5, the transparent area will be filled with black # (or something more conducive to compression) - assert_image_similar(reloaded.convert("RGB"), image, 1) + assert_image_equal(reloaded.convert("RGB"), image) def test_write_unsupported_mode_PA(tmp_path): From 690446050a1963599dc926b06e0360aea0da3f67 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 23:26:08 -0800 Subject: [PATCH 057/727] minor fix in the comments --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 9231150aa55..c2532a49687 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -636,7 +636,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.quality = quality_factor; config.method = method; #if WEBP_ENCODER_ABI_VERSION >= 0x0209 - // the exact flag is only available in libwebp 0.5.0 and later + // the "exact" flag is only available in libwebp 0.5.0 and later config.exact = exact; #endif From d6f10d4876e4f3e1126d3a1d6eac1f75b9d675c2 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Fri, 18 Nov 2022 23:51:06 -0800 Subject: [PATCH 058/727] doc update for libwebp --- 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 9c2319b44e3..ac39625a27a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1127,7 +1127,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exact** If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false. - Requires LibWebP 0.5.0 or later. + Requires libwebp 0.5.0 or later. **icc_profile** The ICC Profile to include in the saved file. Only supported if From 55a75b9a696e968ab56a19a207959211cd33adf9 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sat, 19 Nov 2022 23:14:59 -0800 Subject: [PATCH 059/727] added RN for the new exact option. --- 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 46c7e2f2285..3a9c3977fa2 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -37,6 +37,13 @@ support a ``start`` argument. This tuple of horizontal and vertical offset will be used internally by :py:meth:`.ImageDraw.text` to more accurately place text at the ``xy`` coordinates. +Added the ``exact`` encoding option for WebP +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``exact`` encoding option for WebP is now supported. The WebP encoder +removes the hidden RGB values for better compression by default. By setting +this option to ``True``, the encoder will keep the hidden RGB values. + Security ======== From 9c5b00ef7e03b921fd55cd6c432bac3a00562d46 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sat, 19 Nov 2022 23:19:08 -0800 Subject: [PATCH 060/727] RN trailing space fix --- docs/releasenotes/9.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 3a9c3977fa2..ad79022fef3 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -41,7 +41,7 @@ Added the ``exact`` encoding option for WebP ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``exact`` encoding option for WebP is now supported. The WebP encoder -removes the hidden RGB values for better compression by default. By setting +removes the hidden RGB values for better compression by default. By setting this option to ``True``, the encoder will keep the hidden RGB values. Security From 8f73a895ec29a8f824466812ef6dae9c90ad0397 Mon Sep 17 00:00:00 2001 From: Alireza Shafaei Date: Sun, 20 Nov 2022 16:00:24 -0800 Subject: [PATCH 061/727] Update docs/releasenotes/9.4.0.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/releasenotes/9.4.0.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ad79022fef3..0f47f5ad6d0 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -41,8 +41,9 @@ Added the ``exact`` encoding option for WebP ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``exact`` encoding option for WebP is now supported. The WebP encoder -removes the hidden RGB values for better compression by default. By setting -this option to ``True``, the encoder will keep the hidden RGB values. +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. Security ======== From be7d350e3f5425fe9eaa2bd3fe8e0751d48b1013 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Nov 2022 11:56:30 +1100 Subject: [PATCH 062/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cd1b07be425..461f34e54d0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added "exact" option when saving WebP #6747 + [ashafaei, radarhere] + - Use fractional coordinates when drawing text #6722 [radarhere] From 100ed363ce1407481331d864ae444b22391bb397 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Nov 2022 15:42:44 +1100 Subject: [PATCH 063/727] Updated libpng to 1.6.39 --- 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 10e2000ae68..e4bf275a168 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -228,9 +228,9 @@ def cmd_msbuild( # "bins": [r"libtiff\*.dll"], }, "libpng": { - "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.38/lpng1638.zip/download", - "filename": "lpng1638.zip", - "dir": "lpng1638", + "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download", + "filename": "lpng1639.zip", + "dir": "lpng1639", "license": "LICENSE", "build": [ # lint: do not inline From 2c513c6448d24e393de63be85ec30e980b99d8fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Nov 2022 22:05:57 +1100 Subject: [PATCH 064/727] Use stdlib for setuptools on Cygwin --- .github/workflows/test-cygwin.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 5b9ab0edab3..bbf0ee73696 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.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 @@ -76,7 +76,7 @@ jobs: - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" run: | - .ci/build.sh + SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh - name: Test run: | From 73b91f58d0bfa387880ab94ccb20afd08f634c53 Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 15:10:05 +0000 Subject: [PATCH 065/727] 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 16b9a2eddb5..58ca23a5699 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 066/727] 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 58ca23a5699..68afb988a4c 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 067/727] 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 68afb988a4c..9b4b1d8f50c 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -365,7 +365,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 4a36d9d761790d88c98e08dd273dbc7abb0c71b2 Mon Sep 17 00:00:00 2001 From: Javier Dehesa Date: Fri, 25 Nov 2022 22:27:18 +0000 Subject: [PATCH 068/727] 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 9b4b1d8f50c..e16f33eb085 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 851e7b03ec2a1ccbc98c2d4fcb765e932187c985 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Nov 2022 22:57:10 +1100 Subject: [PATCH 069/727] Document how to install Pillow from a directory --- docs/installation.rst | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index cf6b9ca8fb5..c50a6cc3c0a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -103,10 +103,6 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems: Building From Source -------------------- -Download and extract the `compressed archive from PyPI`_. - -.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ - .. _external-libraries: External Libraries @@ -191,7 +187,8 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Once you have installed the prerequisites, run:: +Once you have installed the prerequisites, to install Pillow from the source +code on PyPI, run:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow --no-binary :all: @@ -211,6 +208,16 @@ prerequisites, it may be necessary to manually clear the pip cache or build without cache using the ``--no-cache-dir`` option to force a build with newly installed external libraries. +If you would like to install from a local copy of the source code instead, you +can download and extract the `compressed archive from PyPI`_, or clone from +GitHub with ``git clone https://github.com/python-pillow/Pillow``. + +After navigating to the Pillow directory, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install . + +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ Build Options ^^^^^^^^^^^^^ From 58cbcbf10826039376b563521b24b84e3fddbb8f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Nov 2022 08:47:40 +1100 Subject: [PATCH 070/727] Added getxmp() to WebPImagePlugin --- Tests/test_file_webp_metadata.py | 21 +++++++++++++++++++++ src/PIL/WebPImagePlugin.py | 3 +++ 2 files changed, 24 insertions(+) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index f77a245c035..4f513d82bc8 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -11,6 +11,11 @@ skip_unless_feature("webp_mux"), ] +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + def test_read_exif_metadata(): @@ -110,6 +115,22 @@ def test_read_no_exif(): assert not webp_image._getexif() +def test_getxmp(): + with Image.open("Tests/images/flower.webp") as im: + assert "xmp" not in im.info + assert im.getxmp() == {} + + with Image.open("Tests/images/flower2.webp") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + assert ( + im.getxmp()["xmpmeta"]["xmptk"] + == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 " + ) + + @skip_unless_feature("webp_anim") def test_write_animated_metadata(tmp_path): iccp_data = b"" diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index e3c19db3dbf..e9a7aac77ed 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -98,6 +98,9 @@ def _getexif(self): return None return self.getexif()._get_merged_dict() + def getxmp(self): + return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} + def seek(self, frame): if not self._seek_check(frame): return From 3473eb8e7f225e6d2fc3709b0565289d64ff9cec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 15:44:03 +1100 Subject: [PATCH 071/727] Added Exif hide_offsets() --- Tests/test_image.py | 25 +++++++++++++++++++++++++ src/PIL/Image.py | 19 ++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index e579034904d..45fedbe4ddb 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -838,6 +838,31 @@ def test_exif_load_from_fp(self): 34665: 196, } + def test_exif_hide_offsets(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + + # Check offsets are present initially + assert 0x8769 in exif + for tag in (0xA005, 0x927C): + assert tag in exif.get_ifd(0x8769) + assert exif.get_ifd(0xA005) + loaded_exif = exif + + with Image.open("Tests/images/flower.jpg") as im: + new_exif = im.getexif() + + for exif in (loaded_exif, new_exif): + exif.hide_offsets() + + # Assert they are hidden afterwards, + # but that the IFDs are still available + assert 0x8769 not in exif + assert exif.get_ifd(0x8769) + for tag in (0xA005, 0x927C): + assert tag not in exif.get_ifd(0x8769) + assert exif.get_ifd(0xA005) + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero_tobytes(self, size): im = Image.new("RGB", size) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c2481b..10ca3a65e82 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3503,6 +3503,7 @@ class Exif(MutableMapping): def __init__(self): self._data = {} + self._hidden_data = {} self._ifds = {} self._info = None self._loaded_exif = None @@ -3556,6 +3557,7 @@ def load(self, data): return self._loaded_exif = data self._data.clear() + self._hidden_data.clear() self._ifds.clear() if data and data.startswith(b"Exif\x00\x00"): data = data[6:] @@ -3576,6 +3578,7 @@ def load(self, data): def load_from_fp(self, fp, offset=None): self._loaded_exif = None self._data.clear() + self._hidden_data.clear() self._ifds.clear() # process dictionary @@ -3631,8 +3634,9 @@ def get_ifd(self, tag): if tag not in self._ifds: if tag in [0x8769, 0x8825]: # exif, gpsinfo - if tag in self: - self._ifds[tag] = self._get_ifd_dict(self[tag]) + offset = self._hidden_data.get(tag, self.get(tag)) + if offset is not None: + self._ifds[tag] = self._get_ifd_dict(offset) elif tag in [0xA005, 0x927C]: # interop, makernote if 0x8769 not in self._ifds: @@ -3717,7 +3721,16 @@ def get_ifd(self, tag): else: # interop self._ifds[tag] = self._get_ifd_dict(tag_data) - return self._ifds.get(tag, {}) + ifd = self._ifds.get(tag, {}) + if tag == 0x8769 and self._hidden_data: + ifd = {k: v for (k, v) in ifd.items() if k not in (0xA005, 0x927C)} + return ifd + + def hide_offsets(self): + for tag in (0x8769, 0x8825): + if tag in self: + self._hidden_data[tag] = self[tag] + del self[tag] def __str__(self): if self._info is not None: From 406a8478cd73c5c166783e9d3583b2249e3b7068 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 17:41:06 +1100 Subject: [PATCH 072/727] 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 e16f33eb085..506bb7008f7 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 710927a311c5699b69d78bcd13ea6ddabc0b0563 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 19:08:49 +1100 Subject: [PATCH 073/727] Added docstring --- src/PIL/WebPImagePlugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index e9a7aac77ed..81ed550d962 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -99,6 +99,12 @@ def _getexif(self): return self.getexif()._get_merged_dict() def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} def seek(self, frame): From 3f9410334cd9efe4d9ebca5eb59d42b7c1c8778c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 19:11:51 +1100 Subject: [PATCH 074/727] Added getxmp() to 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 0f47f5ad6d0..f2b50fa5b31 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. +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +decoded for WEBP images through ``getxmp()``. + Security ======== From 72372ad23f612a320c470c442afb4adab39d988b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Nov 2022 20:42:04 +1100 Subject: [PATCH 075/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 461f34e54d0..7fac5201cc5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added getxmp() to WebPImagePlugin #6758 + [radarhere] + - Added "exact" option when saving WebP #6747 [ashafaei, radarhere] From 24a5405a9f7ea22f28f9c98b3e407292ea5ee1d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Nov 2022 08:39:56 +1100 Subject: [PATCH 076/727] Added IFD enum --- docs/reference/ExifTags.rst | 7 ++++++ src/PIL/ExifTags.py | 7 ++++++ src/PIL/Image.py | 45 ++++++++++++++++++++++++------------- src/PIL/MpoImagePlugin.py | 11 +++++++-- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index d362334a59c..650bb4f9546 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -31,6 +31,13 @@ which provide constants and clear-text names for various well-known EXIF tags. >>> Interop(4096).name 'RelatedImageFileFormat' +.. py:data:: IFD + + >>> from PIL.ExifTags import IFD + >>> IFD.Exif.value + 34665 + >>> IFD(34665).name + 'Exif' Two of these values are also exposed as dictionaries. diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index c00730ba91d..97a21335f14 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -346,3 +346,10 @@ class Interop(IntEnum): RelatedImageFileFormat = 4096 RelatedImageWidth = 4097 RleatedImageHeight = 4098 + + +class IFD(IntEnum): + Exif = 34665 + GPSInfo = 34853 + Makernote = 37500 + Interop = 40965 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c2481b..3fcc869315a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -47,7 +47,14 @@ # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. # Use __version__ instead. -from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins +from . import ( + ExifTags, + ImageMode, + TiffTags, + UnidentifiedImageError, + __version__, + _plugins, +) from ._binary import i32le, o32be, o32le from ._deprecate import deprecate from ._util import DeferredError, is_path @@ -3598,14 +3605,16 @@ def _get_merged_dict(self): merged_dict = dict(self) # get EXIF extension - if 0x8769 in self: - ifd = self._get_ifd_dict(self[0x8769]) + if ExifTags.IFD.Exif in self: + ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif]) if ifd: merged_dict.update(ifd) # GPS - if 0x8825 in self: - merged_dict[0x8825] = self._get_ifd_dict(self[0x8825]) + if ExifTags.IFD.GPSInfo in self: + merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( + self[ExifTags.IFD.GPSInfo] + ) return merged_dict @@ -3615,30 +3624,34 @@ def tobytes(self, offset=8): head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): - if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict): + if tag in [ + ExifTags.IFD.Exif, + 0x8225, + ExifTags.IFD.GPSInfo, + ] and not isinstance(value, dict): value = self.get_ifd(tag) if ( - tag == 0x8769 - and 0xA005 in value - and not isinstance(value[0xA005], dict) + tag == ExifTags.IFD.Exif + and ExifTags.IFD.Interop in value + and not isinstance(value[ExifTags.IFD.Interop], dict) ): value = value.copy() - value[0xA005] = self.get_ifd(0xA005) + value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) def get_ifd(self, tag): if tag not in self._ifds: - if tag in [0x8769, 0x8825]: + if tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: # exif, gpsinfo if tag in self: self._ifds[tag] = self._get_ifd_dict(self[tag]) - elif tag in [0xA005, 0x927C]: + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: # interop, makernote - if 0x8769 not in self._ifds: - self.get_ifd(0x8769) - tag_data = self._ifds[0x8769][tag] - if tag == 0x927C: + if ExifTags.IFD.Exif not in self._ifds: + self.get_ifd(ExifTags.IFD.Exif) + tag_data = self._ifds[ExifTags.IFD.Exif][tag] + if tag == ExifTags.IFD.Makernote: # makernote from .TiffImagePlugin import ImageFileDirectory_v2 diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 92d288f2fb1..3ae4d4abf5b 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -22,7 +22,14 @@ import os import struct -from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin +from . import ( + ExifTags, + Image, + ImageFile, + ImageSequence, + JpegImagePlugin, + TiffImagePlugin, +) from ._binary import i16be as i16 from ._binary import o32le @@ -137,7 +144,7 @@ def seek(self, frame): mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"] if mptype.startswith("Large Thumbnail"): - exif = self.getexif().get_ifd(0x8769) + exif = self.getexif().get_ifd(ExifTags.IFD.Exif) if 40962 in exif and 40963 in exif: self._size = (exif[40962], exif[40963]) elif "exif" in self.info: From a0326245a288801b7ea4753eef32d94398c5b9af Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Nov 2022 21:19:13 +1100 Subject: [PATCH 077/727] Removed typo --- src/PIL/Image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3fcc869315a..d07fc716ce2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3626,7 +3626,6 @@ def tobytes(self, offset=8): for tag, value in self.items(): if tag in [ ExifTags.IFD.Exif, - 0x8225, ExifTags.IFD.GPSInfo, ] and not isinstance(value, dict): value = self.get_ifd(tag) From 50cdf39f505158e37af2cbe39458f3ab27e7377e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Nov 2022 09:18:47 +1100 Subject: [PATCH 078/727] List dependency instructions first --- docs/installation.rst | 186 ++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 99 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c50a6cc3c0a..af1d3399c31 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -187,85 +187,8 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Once you have installed the prerequisites, to install Pillow from the source -code on PyPI, run:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - -If the prerequisites are installed in the standard library locations -for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no -additional configuration should be required. If they are installed in -a non-standard location, you may need to configure setuptools to use -those locations by editing :file:`setup.py` or -:file:`setup.cfg`, or by adding environment variables on the command -line:: - - CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: - -If Pillow has been previously built without the required -prerequisites, it may be necessary to manually clear the pip cache or -build without cache using the ``--no-cache-dir`` option to force a -build with newly installed external libraries. - -If you would like to install from a local copy of the source code instead, you -can download and extract the `compressed archive from PyPI`_, or clone from -GitHub with ``git clone https://github.com/python-pillow/Pillow``. - -After navigating to the Pillow directory, run:: - - python3 -m pip install --upgrade pip - python3 -m pip install . - -.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ - -Build Options -^^^^^^^^^^^^^ - -* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` - sets the number of CPUs to use, or can disable parallel building by - using a setting of 1. By default, it uses 4 CPUs, or if 4 are not - available, as many as are present. - -* Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-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``. - 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`` - These flags are used to compile a modified version of libraqm and - a shim that dynamically loads libfribidi at runtime. These are - used to compile the standard Pillow wheels. Compiling libraqm requires - a C99-compliant compiler. - -* Build flag: ``--disable-platform-guessing``. Skips all of the - platform dependent guessing of include and library directories for - automated build systems that configure the proper paths in the - environment variables (e.g. Buildroot). - -* Build flag: ``--debug``. Adds a debugging flag to the include and - library search process to dump all paths searched for and found to - stdout. - - -Sample usage:: - - python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" - - Building on macOS -^^^^^^^^^^^^^^^^^ +""""""""""""""""" The Xcode command line tools are required to compile portions of Pillow. The tools are installed by running ``xcode-select --install`` @@ -285,25 +208,19 @@ To install libraqm on macOS use Homebrew to install its dependencies:: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -Now install Pillow with:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - -or from within the uncompressed source directory:: - - python3 -m pip install . - Building on Windows -^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""" We recommend you use prebuilt wheels from PyPI. If you wish to compile Pillow manually, you can use the build scripts in the ``winbuild`` directory used for CI testing and development. These scripts require Visual Studio 2017 or newer and NASM. +The scripts also install Pillow from the local copy of the source code, so the +`Installing`_ instructions will not be necessary afterwards. + Building on Windows using MSYS2/MinGW -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""""""""""""""""""""" To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. @@ -332,14 +249,8 @@ Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libraqm -Now install Pillow with:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - - Building on FreeBSD -^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""" .. Note:: Only FreeBSD 10 and 11 tested @@ -353,9 +264,8 @@ Prerequisites are installed on **FreeBSD 10 or 11** with:: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. - Building on Linux -^^^^^^^^^^^^^^^^^ +""""""""""""""""" If you didn't build Python from source, make sure you have Python's development libraries installed. @@ -403,7 +313,7 @@ See also the ``Dockerfile``\s in the Test Infrastructure repo install process for other tested distros. Building on Android -^^^^^^^^^^^^^^^^^^^ +""""""""""""""""""" Basic Android support has been added for compilation within the Termux environment. The dependencies can be installed by:: @@ -413,6 +323,84 @@ environment. The dependencies can be installed by:: This has been tested within the Termux app on ChromeOS, on x86. +Installing +^^^^^^^^^^ + +Once you have installed the prerequisites, to install Pillow from the source +code on PyPI, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow --no-binary :all: + +If the prerequisites are installed in the standard library locations +for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no +additional configuration should be required. If they are installed in +a non-standard location, you may need to configure setuptools to use +those locations by editing :file:`setup.py` or +:file:`setup.cfg`, or by adding environment variables on the command +line:: + + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: + +If Pillow has been previously built without the required +prerequisites, it may be necessary to manually clear the pip cache or +build without cache using the ``--no-cache-dir`` option to force a +build with newly installed external libraries. + +If you would like to install from a local copy of the source code instead, you +can download and extract the `compressed archive from PyPI`_, or clone from +GitHub with ``git clone https://github.com/python-pillow/Pillow``. + +After navigating to the Pillow directory, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install . + +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ + +Build Options +""""""""""""" + +* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use + multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` + sets the number of CPUs to use, or can disable parallel building by + using a setting of 1. By default, it uses 4 CPUs, or if 4 are not + available, as many as are present. + +* Build flags: ``--disable-zlib``, ``--disable-jpeg``, + ``--disable-tiff``, ``--disable-freetype``, ``--disable-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``. + 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`` + These flags are used to compile a modified version of libraqm and + a shim that dynamically loads libfribidi at runtime. These are + used to compile the standard Pillow wheels. Compiling libraqm requires + a C99-compliant compiler. + +* Build flag: ``--disable-platform-guessing``. Skips all of the + platform dependent guessing of include and library directories for + automated build systems that configure the proper paths in the + environment variables (e.g. Buildroot). + +* Build flag: ``--debug``. Adds a debugging flag to the include and + library search process to dump all paths searched for and found to + stdout. + + +Sample usage:: + + python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" Platform Support ---------------- From 556b672eb2f982a90e271a580d2a1f00b78ca131 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 27 Nov 2022 17:48:12 -0600 Subject: [PATCH 079/727] Fix webp dealloc method definitions --- src/_webp.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index c2532a49687..493e0709c46 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -178,12 +178,11 @@ _anim_encoder_new(PyObject *self, PyObject *args) { return NULL; } -PyObject * +void _anim_encoder_dealloc(PyObject *self) { WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPPictureFree(&(encp->frame)); WebPAnimEncoderDelete(encp->enc); - Py_RETURN_NONE; } PyObject * @@ -400,12 +399,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) { return NULL; } -PyObject * +void _anim_decoder_dealloc(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPDataClear(&(decp->data)); WebPAnimDecoderDelete(decp->dec); - Py_RETURN_NONE; } PyObject * From 91fe817911cd8f4a4fa30632aeff621e495cfa7f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 28 Nov 2022 18:03:08 +1100 Subject: [PATCH 080/727] Updated instructions to download source code Co-authored-by: Hugo van Kemenade --- docs/installation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index af1d3399c31..6d67a253656 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -348,15 +348,15 @@ build without cache using the ``--no-cache-dir`` option to force a build with newly installed external libraries. If you would like to install from a local copy of the source code instead, you -can download and extract the `compressed archive from PyPI`_, or clone from -GitHub with ``git clone https://github.com/python-pillow/Pillow``. +can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` +or download and extract the `compressed archive from PyPI`_. After navigating to the Pillow directory, run:: python3 -m pip install --upgrade pip python3 -m pip install . -.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files Build Options """"""""""""" From e3a46fcfd0111d7f080da0efe5846430771afeeb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 11:08:39 +0200 Subject: [PATCH 081/727] Use sphinx-inline-tabs to organise installation per OS --- .editorconfig | 4 + docs/Makefile | 2 +- docs/conf.py | 7 +- docs/installation.rst | 263 +++++++++++++++++++++--------------------- setup.cfg | 1 + 5 files changed, 142 insertions(+), 135 deletions(-) diff --git a/.editorconfig b/.editorconfig index d74549fe2ac..7f5eab05673 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,10 @@ indent_style = space trim_trailing_whitespace = true +[*.rst] +# Three-space indentation +indent_size = 3 + [*.yml] # Two-space indentation indent_size = 2 diff --git a/docs/Makefile b/docs/Makefile index 458299aac7b..0a663ce2b0c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -43,7 +43,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph html: $(MAKE) install-sphinx diff --git a/docs/conf.py b/docs/conf.py index bc67d936893..04823e2d7c7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,12 +27,13 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "sphinx_copybutton", - "sphinx_issues", - "sphinx_removed_in", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", + "sphinx_copybutton", + "sphinx_inline_tabs", + "sphinx_issues", + "sphinx_removed_in", "sphinxext.opengraph", ] diff --git a/docs/installation.rst b/docs/installation.rst index 6d67a253656..3c86f09cc27 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -23,6 +23,11 @@ Pillow supports these Python versions. :file: older-versions.csv :header-rows: 1 +.. _Windows Installation: +.. _macOS Installation: +.. _Linux Installation: +.. _FreeBSD Installation: + Basic Installation ------------------ @@ -38,67 +43,69 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade Pillow -Windows Installation -^^^^^^^^^^^^^^^^^^^^ +.. tab:: Windows -We provide Pillow binaries for Windows compiled for the matrix of -supported Pythons in both 32 and 64-bit versions in the wheel format. -These binaries include support for all optional libraries except -libimagequant and libxcb. Raqm support requires -FriBiDi to be installed separately:: + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow -To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. +.. tab:: macOS -macOS Installation -^^^^^^^^^^^^^^^^^^ + We provide binaries for macOS for each of the supported Python + versions in the wheel format. These include support for all optional + libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: -We provide binaries for macOS for each of the supported Python -versions in the wheel format. These include support for all optional -libraries except libimagequant. Raqm support requires -FriBiDi to be installed separately:: + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow +.. tab:: Linux -Linux Installation -^^^^^^^^^^^^^^^^^^ + We provide binaries for Linux for each of the supported Python + versions in the manylinux wheel format. These include support for all + optional libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: -We provide binaries for Linux for each of the supported Python -versions in the manylinux wheel format. These include support for all -optional libraries except libimagequant. Raqm support requires -FriBiDi to be installed separately:: + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + Most major Linux distributions, including Fedora, Ubuntu and ArchLinux + also include Pillow in packages that previously contained PIL e.g. + ``python-imaging``. Debian splits it into two packages, ``python3-pil`` + and ``python3-pil.imagetk``. -Most major Linux distributions, including Fedora, Ubuntu and ArchLinux -also include Pillow in packages that previously contained PIL e.g. -``python-imaging``. Debian splits it into two packages, ``python3-pil`` -and ``python3-pil.imagetk``. +.. tab:: FreeBSD -FreeBSD Installation -^^^^^^^^^^^^^^^^^^^^ + Pillow can be installed on FreeBSD via the official Ports or Packages systems: -Pillow can be installed on FreeBSD via the official Ports or Packages systems: + **Ports**:: -**Ports**:: + cd /usr/ports/graphics/py-pillow && make install clean - cd /usr/ports/graphics/py-pillow && make install clean + **Packages**:: -**Packages**:: + pkg install py38-pillow - pkg install py38-pillow + .. note:: -.. note:: + The `Pillow FreeBSD port + `_ and packages + are tested by the ports team with all supported FreeBSD versions. - The `Pillow FreeBSD port - `_ and packages - are tested by the ports team with all supported FreeBSD versions. +.. _Building on macOS: +.. _Building on Windows: +.. _Building on Windows using MSYS2/MinGW: +.. _Building on FreeBSD: +.. _Building on Linux: +.. _Building on Android: Building From Source -------------------- @@ -187,141 +194,135 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Building on macOS -""""""""""""""""" +.. tab:: macOS -The Xcode command line tools are required to compile portions of -Pillow. The tools are installed by running ``xcode-select --install`` -from the command line. The command line tools are required even if you -have the full Xcode package installed. It may be necessary to run -``sudo xcodebuild -license`` to accept the license prior to using the -tools. + The Xcode command line tools are required to compile portions of + Pillow. The tools are installed by running ``xcode-select --install`` + from the command line. The command line tools are required even if you + have the full Xcode package installed. It may be necessary to run + ``sudo xcodebuild -license`` to accept the license prior to using the + tools. -The easiest way to install external libraries is via `Homebrew -`_. After you install Homebrew, run:: + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp + brew install libjpeg libtiff little-cms2 openjpeg webp -To install libraqm on macOS use Homebrew to install its dependencies:: + To install libraqm on macOS use Homebrew to install its dependencies:: - brew install freetype harfbuzz fribidi + brew install freetype harfbuzz fribidi -Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -Building on Windows -""""""""""""""""""" +.. tab:: Windows -We recommend you use prebuilt wheels from PyPI. -If you wish to compile Pillow manually, you can use the build scripts -in the ``winbuild`` directory used for CI testing and development. -These scripts require Visual Studio 2017 or newer and NASM. + We recommend you use prebuilt wheels from PyPI. + If you wish to compile Pillow manually, you can use the build scripts + in the ``winbuild`` directory used for CI testing and development. + These scripts require Visual Studio 2017 or newer and NASM. -The scripts also install Pillow from the local copy of the source code, so the -`Installing`_ instructions will not be necessary afterwards. + The scripts also install Pillow from the local copy of the source code, so the + `Installing`_ instructions will not be necessary afterwards. -Building on Windows using MSYS2/MinGW -""""""""""""""""""""""""""""""""""""" +.. tab:: Windows using MSYS2/MinGW -To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or -**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. -The following instructions target the 64-bit build, for 32-bit -replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. -Make sure you have Python and GCC installed:: + Make sure you have Python and GCC installed:: - pacman -S \ - mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + pacman -S \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-python3 \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools -Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: + Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: - pacman -S \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-zlib \ - mingw-w64-x86_64-libtiff \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm + pacman -S \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-zlib \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libraqm -Building on FreeBSD -""""""""""""""""""" +.. tab:: FreeBSD -.. Note:: Only FreeBSD 10 and 11 tested + .. Note:: Only FreeBSD 10 and 11 tested -Make sure you have Python's development libraries installed:: + Make sure you have Python's development libraries installed:: - sudo pkg install python3 + sudo pkg install python3 -Prerequisites are installed on **FreeBSD 10 or 11** with:: + Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb -Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -Building on Linux -""""""""""""""""" +.. tab:: Linux -If you didn't build Python from source, make sure you have Python's -development libraries installed. + If you didn't build Python from source, make sure you have Python's + development libraries installed. -In Debian or Ubuntu:: + In Debian or Ubuntu:: - sudo apt-get install python3-dev python3-setuptools + sudo apt-get install python3-dev python3-setuptools -In Fedora, the command is:: + In Fedora, the command is:: - sudo dnf install python3-devel redhat-rpm-config + sudo dnf install python3-devel redhat-rpm-config -In Alpine, the command is:: + In Alpine, the command is:: - sudo apk add python3-dev py3-setuptools + sudo apk add python3-dev py3-setuptools -.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. -Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev -To install libraqm, ``sudo apt-get install meson`` and then see -``depends/install_raqm.sh``. + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. -Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel -Note that the package manager may be yum or DNF, depending on the -exact distribution. + Note that the package manager may be yum or DNF, depending on the + exact distribution. -Prerequisites are installed for **Alpine** with:: + Prerequisites are installed for **Alpine** with:: - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev -See also the ``Dockerfile``\s in the Test Infrastructure repo -(https://github.com/python-pillow/docker-images) for a known working -install process for other tested distros. + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. -Building on Android -""""""""""""""""""" +.. tab:: Android -Basic Android support has been added for compilation within the Termux -environment. The dependencies can be installed by:: + Basic Android support has been added for compilation within the Termux + environment. The dependencies can be installed by:: - pkg install -y python ndk-sysroot clang make \ - libjpeg-turbo + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo -This has been tested within the Termux app on ChromeOS, on x86. + This has been tested within the Termux app on ChromeOS, on x86. Installing ^^^^^^^^^^ diff --git a/setup.cfg b/setup.cfg index 44feb25ff7f..b562e293471 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ docs = olefile sphinx>=2.4 sphinx-copybutton + sphinx-inline-tabs sphinx-issues>=3.0.1 sphinx-removed-in sphinxext-opengraph From e6e5a0018e27779827678552ea2b17a7c1034e7e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:34:43 +0200 Subject: [PATCH 082/727] Add missing 'make help' for serve and livehtml --- docs/Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Makefile b/docs/Makefile index 0a663ce2b0c..a65e2d3f58e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -20,6 +20,8 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" + @echo " serve to start a local server for viewing docs" + @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @@ -38,6 +40,8 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " serve to start a local server for viewing docs" + @echo " livehtml to start a local server for viewing docs and auto-reload on change" clean: -rm -rf $(BUILDDIR)/* From d12c119ec41e62642157a2add640fc0211d46066 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:36:04 +0200 Subject: [PATCH 083/727] Inline PHONY targets to help avoid omissions (texinfo, info, livehtml, serve were missing) --- docs/Makefile | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index a65e2d3f58e..d5242f93510 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,8 +15,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - +.PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @@ -40,45 +39,50 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " serve to start a local server for viewing docs" - @echo " livehtml to start a local server for viewing docs and auto-reload on change" +.PHONY: clean clean: -rm -rf $(BUILDDIR)/* install-sphinx: $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph +.PHONY: html html: $(MAKE) install-sphinx $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." +.PHONY: dirhtml dirhtml: $(MAKE) install-sphinx $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." +.PHONY: singlehtml singlehtml: $(MAKE) install-sphinx $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." +.PHONY: pickle pickle: $(MAKE) install-sphinx $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." +.PHONY: json json: $(MAKE) install-sphinx $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." +.PHONY: htmlhelp htmlhelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @@ -86,6 +90,7 @@ htmlhelp: @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." +.PHONY: qthelp qthelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @@ -96,6 +101,7 @@ qthelp: @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" +.PHONY: devhelp devhelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @@ -106,12 +112,14 @@ devhelp: @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork" @echo "# devhelp" +.PHONY: epub epub: $(MAKE) install-sphinx $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." +.PHONY: latex latex: $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @@ -120,6 +128,7 @@ latex: @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." +.PHONY: latexpdf latexpdf: $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @@ -127,18 +136,21 @@ latexpdf: $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." +.PHONY: text text: $(MAKE) install-sphinx $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." +.PHONY: man man: $(MAKE) install-sphinx $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." +.PHONY: texinfo texinfo: $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @@ -147,6 +159,7 @@ texinfo: @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." +.PHONY: info info: $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @@ -154,18 +167,21 @@ info: make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." +.PHONY: gettext gettext: $(MAKE) install-sphinx $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." +.PHONY: changes changes: $(MAKE) install-sphinx $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." +.PHONY: linkcheck linkcheck: $(MAKE) install-sphinx $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @@ -173,14 +189,17 @@ linkcheck: @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." +.PHONY: doctest doctest: $(MAKE) install-sphinx $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." +.PHONY: livehtml livehtml: html livereload $(BUILDDIR)/html -p 33233 +.PHONY: serve serve: cd $(BUILDDIR)/html; $(PYTHON) -m http.server From 5e42b1779e29ecbd34adcdd911b44cbe87ad439b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 12:50:55 +0200 Subject: [PATCH 084/727] Reorder tabs: big three OS first --- docs/installation.rst | 128 +++++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 3c86f09cc27..00924eab9b3 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -23,9 +23,9 @@ Pillow supports these Python versions. :file: older-versions.csv :header-rows: 1 -.. _Windows Installation: -.. _macOS Installation: .. _Linux Installation: +.. _macOS Installation: +.. _Windows Installation: .. _FreeBSD Installation: Basic Installation @@ -43,18 +43,20 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade Pillow -.. tab:: Windows +.. tab:: Linux - We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in both 32 and 64-bit versions in the wheel format. - These binaries include support for all optional libraries except - libimagequant and libxcb. Raqm support requires + We provide binaries for Linux for each of the supported Python + versions in the manylinux wheel format. These include support for all + optional libraries except libimagequant. Raqm support requires FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow - To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + Most major Linux distributions, including Fedora, Ubuntu and ArchLinux + also include Pillow in packages that previously contained PIL e.g. + ``python-imaging``. Debian splits it into two packages, ``python3-pil`` + and ``python3-pil.imagetk``. .. tab:: macOS @@ -66,20 +68,18 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow -.. tab:: Linux +.. tab:: Windows - We provide binaries for Linux for each of the supported Python - versions in the manylinux wheel format. These include support for all - optional libraries except libimagequant. Raqm support requires + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow - Most major Linux distributions, including Fedora, Ubuntu and ArchLinux - also include Pillow in packages that previously contained PIL e.g. - ``python-imaging``. Debian splits it into two packages, ``python3-pil`` - and ``python3-pil.imagetk``. + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. .. tab:: FreeBSD @@ -100,11 +100,11 @@ Install Pillow with :command:`pip`:: are tested by the ports team with all supported FreeBSD versions. +.. _Building on Linux: .. _Building on macOS: .. _Building on Windows: .. _Building on Windows using MSYS2/MinGW: .. _Building on FreeBSD: -.. _Building on Linux: .. _Building on Android: Building From Source @@ -194,6 +194,53 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. +.. tab:: Linux + + If you didn't build Python from source, make sure you have Python's + development libraries installed. + + In Debian or Ubuntu:: + + sudo apt-get install python3-dev python3-setuptools + + In Fedora, the command is:: + + sudo dnf install python3-devel redhat-rpm-config + + In Alpine, the command is:: + + sudo apk add python3-dev py3-setuptools + + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev + + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. + + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + + Note that the package manager may be yum or DNF, depending on the + exact distribution. + + Prerequisites are installed for **Alpine** with:: + + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev + + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. + .. tab:: macOS The Xcode command line tools are required to compile portions of @@ -267,53 +314,6 @@ Many of Pillow's features require external libraries: Then see ``depends/install_raqm_cmake.sh`` to install libraqm. -.. tab:: Linux - - If you didn't build Python from source, make sure you have Python's - development libraries installed. - - In Debian or Ubuntu:: - - sudo apt-get install python3-dev python3-setuptools - - In Fedora, the command is:: - - sudo dnf install python3-devel redhat-rpm-config - - In Alpine, the command is:: - - sudo apk add python3-dev py3-setuptools - - .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - - Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev - - To install libraqm, ``sudo apt-get install meson`` and then see - ``depends/install_raqm.sh``. - - Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel - - Note that the package manager may be yum or DNF, depending on the - exact distribution. - - Prerequisites are installed for **Alpine** with:: - - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev - - See also the ``Dockerfile``\s in the Test Infrastructure repo - (https://github.com/python-pillow/docker-images) for a known working - install process for other tested distros. - .. tab:: Android Basic Android support has been added for compilation within the Termux From 50ccb27a4d57500b9b5e474e276df03158b6c870 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 28 Nov 2022 23:23:46 +0200 Subject: [PATCH 085/727] Remove extra space Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index d5242f93510..d32d25a3c49 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -90,7 +90,7 @@ htmlhelp: @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." -.PHONY: qthelp +.PHONY: qthelp qthelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp From 3ec8fa614705ae273426d60f994e3b01bb57a69a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 30 Nov 2022 13:49:07 +1100 Subject: [PATCH 086/727] Do not trust JPEG decoder to determine image is CMYK --- src/PIL/BlpImagePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 53399773716..45987ec03a7 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -373,6 +373,9 @@ def _decode_jpeg_stream(self): data = BytesIO(data) image = JpegImageFile(data) Image._decompression_bomb_check(image.size) + if image.mode == "CMYK": + decoder_name, extents, offset, args = image.tile[0] + image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))] r, g, b = image.convert("RGB").split() image = Image.merge("RGB", (b, g, r)) self.set_as_raw(image.tobytes()) From aab7983146729c81a5105b6511858f41d76b53f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Dec 2022 07:57:26 +1100 Subject: [PATCH 087/727] Updated xz to 5.2.9 --- 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 e4bf275a168..66e352c73c8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ def cmd_msbuild( "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.8.tar.gz/download", - "filename": "xz-5.2.8.tar.gz", - "dir": "xz-5.2.8", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.9.tar.gz/download", + "filename": "xz-5.2.9.tar.gz", + "dir": "xz-5.2.9", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From 96b316880e284f4221b2d8400ef1bec5c81bcb83 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 2 Dec 2022 11:40:06 +0200 Subject: [PATCH 088/727] Use 4-space indents for RST --- .editorconfig | 2 +- docs/installation.rst | 218 +++++++++++++++++++++--------------------- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7f5eab05673..07f02c23665 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,7 +15,7 @@ trim_trailing_whitespace = true [*.rst] # Three-space indentation -indent_size = 3 +indent_size = 4 [*.yml] # Two-space indentation diff --git a/docs/installation.rst b/docs/installation.rst index 00924eab9b3..89b2e558f40 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -45,59 +45,59 @@ Install Pillow with :command:`pip`:: .. tab:: Linux - We provide binaries for Linux for each of the supported Python - versions in the manylinux wheel format. These include support for all - optional libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: + We provide binaries for Linux for each of the supported Python + versions in the manylinux wheel format. These include support for all + optional libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow - Most major Linux distributions, including Fedora, Ubuntu and ArchLinux - also include Pillow in packages that previously contained PIL e.g. - ``python-imaging``. Debian splits it into two packages, ``python3-pil`` - and ``python3-pil.imagetk``. + Most major Linux distributions, including Fedora, Ubuntu and ArchLinux + also include Pillow in packages that previously contained PIL e.g. + ``python-imaging``. Debian splits it into two packages, ``python3-pil`` + and ``python3-pil.imagetk``. .. tab:: macOS - We provide binaries for macOS for each of the supported Python - versions in the wheel format. These include support for all optional - libraries except libimagequant. Raqm support requires - FriBiDi to be installed separately:: + We provide binaries for macOS for each of the supported Python + versions in the wheel format. These include support for all optional + libraries except libimagequant. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow .. tab:: Windows - We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in both 32 and 64-bit versions in the wheel format. - These binaries include support for all optional libraries except - libimagequant and libxcb. Raqm support requires - FriBiDi to be installed separately:: + We provide Pillow binaries for Windows compiled for the matrix of + supported Pythons in both 32 and 64-bit versions in the wheel format. + These binaries include support for all optional libraries except + libimagequant and libxcb. Raqm support requires + FriBiDi to be installed separately:: - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade Pillow - To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. + To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. .. tab:: FreeBSD - Pillow can be installed on FreeBSD via the official Ports or Packages systems: + Pillow can be installed on FreeBSD via the official Ports or Packages systems: - **Ports**:: + **Ports**:: - cd /usr/ports/graphics/py-pillow && make install clean + cd /usr/ports/graphics/py-pillow && make install clean - **Packages**:: + **Packages**:: - pkg install py38-pillow + pkg install py38-pillow - .. note:: + .. note:: - The `Pillow FreeBSD port - `_ and packages - are tested by the ports team with all supported FreeBSD versions. + The `Pillow FreeBSD port + `_ and packages + are tested by the ports team with all supported FreeBSD versions. .. _Building on Linux: @@ -196,133 +196,133 @@ Many of Pillow's features require external libraries: .. tab:: Linux - If you didn't build Python from source, make sure you have Python's - development libraries installed. + If you didn't build Python from source, make sure you have Python's + development libraries installed. - In Debian or Ubuntu:: + In Debian or Ubuntu:: - sudo apt-get install python3-dev python3-setuptools + sudo apt-get install python3-dev python3-setuptools - In Fedora, the command is:: + In Fedora, the command is:: sudo dnf install python3-devel redhat-rpm-config - In Alpine, the command is:: + In Alpine, the command is:: sudo apk add python3-dev py3-setuptools - .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev - To install libraqm, ``sudo apt-get install meson`` and then see - ``depends/install_raqm.sh``. + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. - Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel - Note that the package manager may be yum or DNF, depending on the - exact distribution. + Note that the package manager may be yum or DNF, depending on the + exact distribution. - Prerequisites are installed for **Alpine** with:: + Prerequisites are installed for **Alpine** with:: - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev - See also the ``Dockerfile``\s in the Test Infrastructure repo - (https://github.com/python-pillow/docker-images) for a known working - install process for other tested distros. + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. .. tab:: macOS - The Xcode command line tools are required to compile portions of - Pillow. The tools are installed by running ``xcode-select --install`` - from the command line. The command line tools are required even if you - have the full Xcode package installed. It may be necessary to run - ``sudo xcodebuild -license`` to accept the license prior to using the - tools. + The Xcode command line tools are required to compile portions of + Pillow. The tools are installed by running ``xcode-select --install`` + from the command line. The command line tools are required even if you + have the full Xcode package installed. It may be necessary to run + ``sudo xcodebuild -license`` to accept the license prior to using the + tools. - The easiest way to install external libraries is via `Homebrew - `_. After you install Homebrew, run:: + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp + brew install libjpeg libtiff little-cms2 openjpeg webp - To install libraqm on macOS use Homebrew to install its dependencies:: + To install libraqm on macOS use Homebrew to install its dependencies:: - brew install freetype harfbuzz fribidi + brew install freetype harfbuzz fribidi - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. .. tab:: Windows - We recommend you use prebuilt wheels from PyPI. - If you wish to compile Pillow manually, you can use the build scripts - in the ``winbuild`` directory used for CI testing and development. - These scripts require Visual Studio 2017 or newer and NASM. + We recommend you use prebuilt wheels from PyPI. + If you wish to compile Pillow manually, you can use the build scripts + in the ``winbuild`` directory used for CI testing and development. + These scripts require Visual Studio 2017 or newer and NASM. - The scripts also install Pillow from the local copy of the source code, so the - `Installing`_ instructions will not be necessary afterwards. + The scripts also install Pillow from the local copy of the source code, so the + `Installing`_ instructions will not be necessary afterwards. .. tab:: Windows using MSYS2/MinGW - To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or - **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. - The following instructions target the 64-bit build, for 32-bit - replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. - Make sure you have Python and GCC installed:: + Make sure you have Python and GCC installed:: - pacman -S \ - mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + pacman -S \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-python3 \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools - Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: + Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: - pacman -S \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-zlib \ - mingw-w64-x86_64-libtiff \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm + pacman -S \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-zlib \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libraqm .. tab:: FreeBSD - .. Note:: Only FreeBSD 10 and 11 tested + .. Note:: Only FreeBSD 10 and 11 tested - Make sure you have Python's development libraries installed:: + Make sure you have Python's development libraries installed:: - sudo pkg install python3 + sudo pkg install python3 - Prerequisites are installed on **FreeBSD 10 or 11** with:: + Prerequisites are installed on **FreeBSD 10 or 11** with:: - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. .. tab:: Android - Basic Android support has been added for compilation within the Termux - environment. The dependencies can be installed by:: + Basic Android support has been added for compilation within the Termux + environment. The dependencies can be installed by:: - pkg install -y python ndk-sysroot clang make \ - libjpeg-turbo + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo - This has been tested within the Termux app on ChromeOS, on x86. + This has been tested within the Termux app on ChromeOS, on x86. Installing ^^^^^^^^^^ From c120649632391c1f287cae6b7ff7ce7c28ddb20b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Dec 2022 21:29:27 +1100 Subject: [PATCH 089/727] Remove specific number of jobs from comment --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 5cabb662212..e2a9de65c9f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -141,7 +141,7 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_fribidi.cmd" - # trim ~150MB x 9 + # trim ~150MB for each job - name: Optimize build cache if: steps.build-cache.outputs.cache-hit != 'true' run: rmdir /S /Q winbuild\build\src From d822d85af6d9b1c108650832d56b75cd86b2f5a9 Mon Sep 17 00:00:00 2001 From: Sam Mason Date: Fri, 2 Dec 2022 17:57:19 +0000 Subject: [PATCH 090/727] support round-tripping JPEG comments --- Tests/test_file_jpeg.py | 12 ++++++++++++ src/PIL/JpegImagePlugin.py | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fa96e425b8c..94ef595650b 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -87,6 +87,18 @@ def test_app(self): assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + def test_com_write(self): + dummy_text = "this is a test comment" + with Image.open(TEST_FILE) as im: + with BytesIO() as buf: + im.save(buf, format="JPEG") + with Image.open(buf) as im2: + assert im.app['COM'] == im2.app['COM'] + with BytesIO() as buf: + im.save(buf, format="JPEG", comment=dummy_text) + with Image.open(buf) as im2: + assert im2.app['COM'].decode() == dummy_text + def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6ed223bc6f..a6abe8b9f6c 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -44,6 +44,7 @@ from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._binary import o16be as o16 from ._binary import o8 from ._deprecate import deprecate from .JpegPresets import presets @@ -713,6 +714,15 @@ def validate_qtables(qtables): extra = info.get("extra", b"") + comment = info.get("comment") + if comment is None and isinstance(im, JpegImageFile): + comment = im.app.get('COM') + if comment: + if isinstance(comment, str): + comment = comment.encode() + size = o16(2 + len(comment)) + extra += b'\xFF\xFE%s%s' % (size, comment) + icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 From e9f485849157100ddf75f289db6fcb509927706c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Dec 2022 18:07:07 +0000 Subject: [PATCH 091/727] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 4 ++-- src/PIL/JpegImagePlugin.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 94ef595650b..ffaf2cabaf4 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -93,11 +93,11 @@ def test_com_write(self): with BytesIO() as buf: im.save(buf, format="JPEG") with Image.open(buf) as im2: - assert im.app['COM'] == im2.app['COM'] + assert im.app["COM"] == im2.app["COM"] with BytesIO() as buf: im.save(buf, format="JPEG", comment=dummy_text) with Image.open(buf) as im2: - assert im2.app['COM'].decode() == dummy_text + assert im2.app["COM"].decode() == dummy_text def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6abe8b9f6c..cb8a4e57fd8 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -44,8 +44,8 @@ from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._binary import o16be as o16 from ._binary import o8 +from ._binary import o16be as o16 from ._deprecate import deprecate from .JpegPresets import presets @@ -716,12 +716,12 @@ def validate_qtables(qtables): comment = info.get("comment") if comment is None and isinstance(im, JpegImageFile): - comment = im.app.get('COM') + comment = im.app.get("COM") if comment: if isinstance(comment, str): comment = comment.encode() size = o16(2 + len(comment)) - extra += b'\xFF\xFE%s%s' % (size, comment) + extra += b"\xFF\xFE%s%s" % (size, comment) icc_profile = info.get("icc_profile") if icc_profile: From 976ad5746a0155135337efa75648c550705215c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:29:02 +1100 Subject: [PATCH 092/727] Save comments from any image format by default --- src/PIL/JpegImagePlugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index cb8a4e57fd8..c9de714d895 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -714,9 +714,7 @@ def validate_qtables(qtables): extra = info.get("extra", b"") - comment = info.get("comment") - if comment is None and isinstance(im, JpegImageFile): - comment = im.app.get("COM") + comment = info.get("comment", im.info.get("comment")) if comment: if isinstance(comment, str): comment = comment.encode() From c1d0a00943ee6fcc993f47047b798e2b6f9bac6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:31:05 +1100 Subject: [PATCH 093/727] Use _binary instead of struct --- src/PIL/JpegImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index c9de714d895..92dbb3193a0 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -732,7 +732,7 @@ def validate_qtables(qtables): icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] i = 1 for marker in markers: - size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker)) + size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) extra += ( b"\xFF\xE2" + size From 525c01143a8a4e0133908826577ccb54ed829a1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 09:59:22 +1100 Subject: [PATCH 094/727] Test that comment is reread --- Tests/test_file_jpeg.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ffaf2cabaf4..bb4ebb686f8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -86,18 +86,26 @@ def test_app(self): assert len(im.applist) == 2 assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + assert im.app["COM"] == im.info["comment"] - def test_com_write(self): - dummy_text = "this is a test comment" + def test_comment_write(self): with Image.open(TEST_FILE) as im: - with BytesIO() as buf: - im.save(buf, format="JPEG") - with Image.open(buf) as im2: - assert im.app["COM"] == im2.app["COM"] - with BytesIO() as buf: - im.save(buf, format="JPEG", comment=dummy_text) - with Image.open(buf) as im2: - assert im2.app["COM"].decode() == dummy_text + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + + # Test that existing comment is saved by default + out = BytesIO() + im.save(out, format="JPEG") + with Image.open(out) as reloaded: + assert im.info["comment"] == reloaded.info["comment"] + + # Test that a comment argument overrides the default comment + for comment in ("Test comment text", b"Text comment text"): + out = BytesIO() + im.save(out, format="JPEG", comment=comment) + with Image.open(out) as reloaded: + if not isinstance(comment, bytes): + comment = comment.encode() + assert reloaded.info["comment"] == comment def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, From 61cbcaee64a852bc9902d60ab0732f676d6d0e72 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 10:35:01 +1100 Subject: [PATCH 095/727] Changed indentation to be consistent --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 89b2e558f40..b559c824d11 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -205,11 +205,11 @@ Many of Pillow's features require external libraries: In Fedora, the command is:: - sudo dnf install python3-devel redhat-rpm-config + sudo dnf install python3-devel redhat-rpm-config In Alpine, the command is:: - sudo apk add python3-dev py3-setuptools + sudo apk add python3-dev py3-setuptools .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. From eafff0e1396a1b55522c561b7e355c4e7ebaa5b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Dec 2022 10:54:04 +1100 Subject: [PATCH 096/727] Use compile_python_fuzzer --- Tests/oss-fuzz/build.sh | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index 09cc7bc1696..b459ee47a55 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -19,9 +19,7 @@ python3 setup.py build --build-base=/tmp/build install # Build fuzzers in $OUT. for fuzzer in $(find $SRC -name 'fuzz_*.py'); do - fuzzer_basename=$(basename -s .py $fuzzer) - fuzzer_package=${fuzzer_basename}.pkg - pyinstaller \ + compile_python_fuzzer $fuzzer \ --add-binary /usr/local/lib/libjpeg.so.62.3.0:. \ --add-binary /usr/local/lib/libfreetype.so.6:. \ --add-binary /usr/local/lib/liblcms2.so.2:. \ @@ -31,17 +29,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do --add-binary /usr/local/lib/libwebp.so.7:. \ --add-binary /usr/local/lib/libwebpdemux.so.2:. \ --add-binary /usr/local/lib/libwebpmux.so.3:. \ - --add-binary /usr/local/lib/libxcb.so.1:. \ - --distpath $OUT --onefile --name $fuzzer_package $fuzzer - - # Create execution wrapper. - echo "#!/bin/sh -# LLVMFuzzerTestOneInput for fuzzer detection. -this_dir=\$(dirname \"\$0\") -LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \ -ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \ -\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename - chmod u+x $OUT/$fuzzer_basename + --add-binary /usr/local/lib/libxcb.so.1:. done find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@ From 8e70787cf20399f2a88976ad8ad4d3983f81c741 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 Dec 2022 01:44:21 +0000 Subject: [PATCH 097/727] Update cygwin/cygwin-install-action action to v3 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index bbf0ee73696..37dc694c6ca 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v3 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v2 + uses: cygwin/cygwin-install-action@v3 with: platform: x86_64 packages: > From 61f27211c2d50a20ac544ff8ab16c58439c4cbe8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 3 Dec 2022 07:43:58 +0200 Subject: [PATCH 098/727] Fix comment Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 07f02c23665..449530717f9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ indent_style = space trim_trailing_whitespace = true [*.rst] -# Three-space indentation +# Four-space indentation indent_size = 4 [*.yml] From 1ed1a3a971e127b55353bde39cb7ea6bedb45c04 Mon Sep 17 00:00:00 2001 From: Sam Mason Date: Sat, 3 Dec 2022 15:07:37 +0000 Subject: [PATCH 099/727] make sure passing a blank comment removes existing comment --- Tests/test_file_jpeg.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bb4ebb686f8..7a958c7da84 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -98,6 +98,13 @@ def test_comment_write(self): with Image.open(out) as reloaded: assert im.info["comment"] == reloaded.info["comment"] + # Ensure that a blank comment causes any existing comment to be removed + for comment in ("", b"", None): + out = BytesIO() + im.save(out, format="JPEG", comment=comment) + with Image.open(out) as reloaded: + assert "comment" not in reloaded.info + # Test that a comment argument overrides the default comment for comment in ("Test comment text", b"Text comment text"): out = BytesIO() From 8ada23ed04ee18730d44d14dd82b0aabc12a0917 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 09:09:00 +1100 Subject: [PATCH 100/727] Added IFD1 reading --- Tests/test_image.py | 21 ++++++++++++++++++++- src/PIL/ExifTags.py | 1 + src/PIL/Image.py | 10 +++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index e579034904d..b4e81e46613 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,7 +7,14 @@ import pytest -from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features +from PIL import ( + ExifTags, + Image, + ImageDraw, + ImagePalette, + UnidentifiedImageError, + features, +) from .helper import ( assert_image_equal, @@ -808,6 +815,18 @@ def test_exif_interop(self): reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) + def test_exif_ifd1(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif.get_ifd(ExifTags.IFD.IFD1) == { + 513: 2036, + 514: 5448, + 259: 6, + 296: 2, + 282: 180.0, + 283: 180.0, + } + def test_exif_ifd(self): with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 97a21335f14..ffab7e55454 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -353,3 +353,4 @@ class IFD(IntEnum): GPSInfo = 34853 Makernote = 37500 Interop = 40965 + IFD1 = -1 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d07fc716ce2..1f3d4b74f9c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3641,17 +3641,17 @@ def tobytes(self, offset=8): def get_ifd(self, tag): if tag not in self._ifds: - if tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: - # exif, gpsinfo + if tag == ExifTags.IFD.IFD1: + if self._info is not None: + self._ifds[tag] = self._get_ifd_dict(self._info.next) + elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: if tag in self: self._ifds[tag] = self._get_ifd_dict(self[tag]) elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: - # interop, makernote if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) tag_data = self._ifds[ExifTags.IFD.Exif][tag] if tag == ExifTags.IFD.Makernote: - # makernote from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -3727,7 +3727,7 @@ def get_ifd(self, tag): makernote = {0x1101: dict(self._fixup_dict(camerainfo))} self._ifds[tag] = makernote else: - # interop + # Interop self._ifds[tag] = self._get_ifd_dict(tag_data) return self._ifds.get(tag, {}) From e50ae85ea406d86073ca88ffdec469e1e18d7527 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 13:57:26 +1100 Subject: [PATCH 101/727] Use jpeg_write_marker to write comment --- src/PIL/JpegImagePlugin.py | 12 +++++------- src/encode.c | 26 ++++++++++++++++++++++---- src/libImaging/Jpeg.h | 4 ++++ src/libImaging/JpegEncode.c | 13 ++++++++++++- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 92dbb3193a0..7b5b32be0a2 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -714,13 +714,6 @@ def validate_qtables(qtables): extra = info.get("extra", b"") - comment = info.get("comment", im.info.get("comment")) - if comment: - if isinstance(comment, str): - comment = comment.encode() - size = o16(2 + len(comment)) - extra += b"\xFF\xFE%s%s" % (size, comment) - icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 @@ -743,6 +736,10 @@ def validate_qtables(qtables): ) i += 1 + comment = info.get("comment", im.info.get("comment")) or b"" + if isinstance(comment, str): + comment = comment.encode() + # "progressive" is the official name, but older documentation # says "progression" # FIXME: issue a warning if the wrong form is used (post-1.1.7) @@ -765,6 +762,7 @@ def validate_qtables(qtables): dpi[1], subsampling, qtables, + comment, extra, exif, ) diff --git a/src/encode.c b/src/encode.c index 72c7f64d0a3..a2eae81fdae 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1048,6 +1048,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { PyObject *qtables = NULL; unsigned int *qarrays = NULL; int qtablesLen = 0; + char *comment = NULL; + Py_ssize_t comment_size; char *extra = NULL; Py_ssize_t extra_size; char *rawExif = NULL; @@ -1055,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnOy#y#", + "ss|nnnnnnnnOy#y#y#", &mode, &rawmode, &quality, @@ -1067,6 +1069,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { &ydpi, &subsampling, &qtables, + &comment, + &comment_size, &extra, &extra_size, &rawExif, @@ -1090,12 +1094,24 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { return NULL; } - // Freed in JpegEncode, Case 5 + // Freed in JpegEncode, Case 6 qarrays = get_qtables_arrays(qtables, &qtablesLen); + if (comment && comment_size > 0) { + /* malloc check ok, length is from python parsearg */ + char *p = malloc(comment_size); // Freed in JpegEncode, Case 6 + if (!p) { + return ImagingError_MemoryError(); + } + memcpy(p, comment, comment_size); + comment = p; + } else { + comment = NULL; + } + if (extra && extra_size > 0) { /* malloc check ok, length is from python parsearg */ - char *p = malloc(extra_size); // Freed in JpegEncode, Case 5 + char *p = malloc(extra_size); // Freed in JpegEncode, Case 6 if (!p) { return ImagingError_MemoryError(); } @@ -1107,7 +1123,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (rawExif && rawExifLen > 0) { /* malloc check ok, length is from python parsearg */ - char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5 + char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6 if (!pp) { if (extra) { free(extra); @@ -1134,6 +1150,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; + ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment; + ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size; ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; ((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size; ((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index a876d3bb6d9..1d755081871 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -92,6 +92,10 @@ typedef struct { /* in factors of DCTSIZE2 */ int qtablesLen; + /* Comment */ + char *comment; + size_t comment_size; + /* Extra data (to be injected after header) */ char *extra; int extra_size; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index a44debcafe8..b6e3acc951c 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -277,6 +277,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { } case 4: + + if (context->comment_size > 0) { + jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size); + } + state->state++; + + case 5: if (1024 > context->destination.pub.free_in_buffer) { break; } @@ -301,7 +308,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->state++; /* fall through */ - case 5: + case 6: /* Finish compression */ if (context->destination.pub.free_in_buffer < 100) { @@ -310,6 +317,10 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { jpeg_finish_compress(&context->cinfo); /* Clean up */ + if (context->comment) { + free(context->comment); + context->comment = NULL; + } if (context->extra) { free(context->extra); context->extra = NULL; From 72ac7d1ce9e15803e4adb759dbd318b75d652724 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Dec 2022 18:53:28 +1100 Subject: [PATCH 102/727] Corrected default combined frame duration --- Tests/images/duplicate_frame.gif | Bin 0 -> 138 bytes Tests/test_file_gif.py | 16 ++++++++++++++++ src/PIL/GifImagePlugin.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Tests/images/duplicate_frame.gif diff --git a/Tests/images/duplicate_frame.gif b/Tests/images/duplicate_frame.gif new file mode 100644 index 0000000000000000000000000000000000000000..ef0c894a540b0ca3074938666fd22a7d93d1fd0d GIT binary patch literal 138 zcmZ?wbhEHb Date: Mon, 5 Dec 2022 17:46:54 +0000 Subject: [PATCH 103/727] switch to #z for comment parameter * means `comment=None` can be passed directly * no need to conditionally run `str.encode()` * clean up checking of whether a comment is passed --- src/PIL/JpegImagePlugin.py | 4 +--- src/encode.c | 2 +- src/libImaging/JpegEncode.c | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 7b5b32be0a2..ef0be669954 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -736,9 +736,7 @@ def validate_qtables(qtables): ) i += 1 - comment = info.get("comment", im.info.get("comment")) or b"" - if isinstance(comment, str): - comment = comment.encode() + comment = info.get("comment", im.info.get("comment")) # "progressive" is the official name, but older documentation # says "progression" diff --git a/src/encode.c b/src/encode.c index a2eae81fdae..d37cbfbcf71 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1057,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnOy#y#y#", + "ss|nnnnnnnnOz#y#y#", &mode, &rawmode, &quality, diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index b6e3acc951c..2a24eff39ca 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -278,7 +278,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { case 4: - if (context->comment_size > 0) { + if (context->comment) { jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size); } state->state++; From b786ff819a9799974e55db3d639b0de0b49d9b89 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 18:25:01 +0000 Subject: [PATCH 104/727] [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/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0) - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c13fb3b144..d44874bf7cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] @@ -37,7 +37,7 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-merge-conflict - id: check-json From c2a42655e10c7b3888f3c50b49717886296e1720 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Dec 2022 19:30:53 +1100 Subject: [PATCH 105/727] Allow get_child_images to access JPEG thumbnails --- Tests/images/flower_thumbnail.png | Bin 0 -> 35617 bytes Tests/test_file_jpeg.py | 7 +++++ src/PIL/Image.py | 43 ++++++++++++++++++++++++++++++ src/PIL/JpegImagePlugin.py | 1 + src/PIL/TiffImagePlugin.py | 33 ----------------------- 5 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 Tests/images/flower_thumbnail.png diff --git a/Tests/images/flower_thumbnail.png b/Tests/images/flower_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..4a362535f25fcdf10c49c368f4252f48f8d45bd0 GIT binary patch literal 35617 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 RR_6c!002ovPDHLkV1fuV02lxO literal 0 HcmV?d00001 diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fa96e425b8c..9871875565f 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -415,6 +415,13 @@ def test_exif(self): info = im._getexif() assert info[305] == "Adobe Photoshop CS Macintosh" + def test_get_child_images(self): + with Image.open("Tests/images/flower.jpg") as im: + ims = im.get_child_images() + + assert len(ims) == 1 + assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im._getmp() is None diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1f3d4b74f9c..e568e6afa0b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1454,6 +1454,49 @@ def _reload_exif(self): self._exif._loaded = False self.getexif() + def get_child_images(self): + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + for subifd_offset in subifd_offsets: + ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(513): + ifds.append((ifd1, exif._info.next)) + + offset = None + for ifd, ifd_offset in ifds: + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + thumbnailOffset = ifd.get(513) + if thumbnailOffset is not None: + try: + thumbnailOffset += self._exif_offset + except AttributeError: + pass + self.fp.seek(thumbnailOffset) + data = self.fp.read(ifd.get(514)) + fp = io.BytesIO(data) + + with open(fp) as im: + if thumbnailOffset is None: + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + self.fp.seek(offset) + return child_images + def getim(self): """ Returns a capsule that points to the internal image memory. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6ed223bc6f..f2d8c4846f8 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -89,6 +89,7 @@ def APP(self, marker): if "exif" not in self.info: # extract EXIF information (incomplete) self.info["exif"] = s # FIXME: value will change + self._exif_offset = self.fp.tell() - n + 6 elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab9ac5ea23a..aa2a782c28f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1153,39 +1153,6 @@ def tell(self): """Return the current frame number""" return self.__frame - def get_child_images(self): - if SUBIFD not in self.tag_v2: - return [] - child_images = [] - exif = self.getexif() - offset = None - for im_offset in self.tag_v2[SUBIFD]: - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - current_offset = self._fp.tell() - if offset is None: - offset = current_offset - - fp = self._fp - ifd = exif._get_ifd_dict(im_offset) - jpegInterchangeFormat = ifd.get(513) - if jpegInterchangeFormat is not None: - fp.seek(jpegInterchangeFormat) - jpeg_data = fp.read(ifd.get(514)) - - fp = io.BytesIO(jpeg_data) - - with Image.open(fp) as im: - if jpegInterchangeFormat is None: - im._frame_pos = [im_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - self._fp.seek(offset) - return child_images - def getxmp(self): """ Returns a dictionary containing the XMP tags. From 1d780081a620b00007a8fe93db469e5759c86868 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Dec 2022 20:22:12 +1100 Subject: [PATCH 106/727] Free comment when returning early --- src/encode.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/encode.c b/src/encode.c index d37cbfbcf71..e6352cbfe1a 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1113,6 +1113,9 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { /* malloc check ok, length is from python parsearg */ char *p = malloc(extra_size); // Freed in JpegEncode, Case 6 if (!p) { + if (comment) { + free(comment); + } return ImagingError_MemoryError(); } memcpy(p, extra, extra_size); @@ -1125,6 +1128,9 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { /* malloc check ok, length is from python parsearg */ char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6 if (!pp) { + if (comment) { + free(comment); + } if (extra) { free(extra); } From 674ec6ec4dd1083b4666e283459beeba0e422fb4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 6 Dec 2022 20:55:34 +0200 Subject: [PATCH 107/727] Add support for PyPy3.9, drop PyPy3.7 --- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e2a9de65c9f..487c3586f02 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -19,9 +19,9 @@ jobs: architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows - - python-version: "pypy-3.7" + - python-version: "pypy3.8" architecture: "x64" - - python-version: "pypy-3.8" + - python-version: "pypy3.9" architecture: "x64" timeout-minutes: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 831e33c130d..11c7b77be03 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,8 @@ jobs: "ubuntu-latest", ] python-version: [ - "pypy-3.8", - "pypy-3.7", + "pypy3.9", + "pypy3.8", "3.11", "3.10", "3.9", From ccac8540771120bdeb570ec5b7bbfc4e3e9a38dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 21:33:09 +1100 Subject: [PATCH 108/727] 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 5e0eca28be1..1ad4de63f72 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -64,9 +64,13 @@ def test_grabclipboard(self): ) 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 38074cb1b0d..12ad9ad71fe 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 109/727] 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 1ad4de63f72..01442dc69f0 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -68,8 +68,8 @@ def test_grabclipboard(self): 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 12ad9ad71fe..8cf95680995 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 4704cab1a1b4dfc34b0bc0c06bdcdc56b365b69f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 22:16:14 +1100 Subject: [PATCH 110/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7fac5201cc5..f3ad8c797ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Support saving JPEG comments #6774 + [smason, radarhere] + - Added getxmp() to WebPImagePlugin #6758 [radarhere] From bef128b04bcc220aa6b57afa58b796f7b289ddf7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Dec 2022 22:30:15 +1100 Subject: [PATCH 111/727] Added support for saving JPEG comments --- docs/handbook/image-file-formats.rst | 5 +++++ docs/releasenotes/9.4.0.rst | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ac39625a27a..c9e32835aae 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -474,6 +474,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 2.5.0 +**comment** + A comment about the image. + + .. versionadded:: 9.4.0 + .. note:: diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index f2b50fa5b31..ccbe62a6bcd 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -51,6 +51,14 @@ getxmp() `XMP data `_ can now be decoded for WEBP images through ``getxmp()``. +Writing JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When saving a JPEG image, a comment can now be written from +:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving:: + + im.save(out, comment="Test comment") + Security ======== From 4ab837ae23103b841d2e5fa7ac91a8ff92279627 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Dec 2022 11:35:48 +1100 Subject: [PATCH 112/727] Only compare to previous when checking for duplicate frames while saving --- Tests/test_file_gif.py | 18 ++++++++++++++++++ src/PIL/GifImagePlugin.py | 32 +++++++++++++++++--------------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 926f5c1eea8..2cbaf2805de 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -677,6 +677,24 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == (255, 0, 0) +def test_dispose2_background_frame(tmp_path): + out = str(tmp_path / "temp.gif") + + im_list = [Image.new("RGBA", (1, 20))] + + different_frame = Image.new("RGBA", (1, 20)) + different_frame.putpixel((0, 10), (255, 0, 0, 255)) + im_list.append(different_frame) + + # Frame that matches the background + im_list.append(Image.new("RGBA", (1, 20))) + + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + + with Image.open(out) as im: + assert im.n_frames == 3 + + def test_transparency_in_second_frame(tmp_path): out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dd1b21f2e63..3679580481e 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -565,6 +565,16 @@ def _write_single_frame(im, fp, palette): fp.write(b"\0") # end of image data +def _getbbox(base_im, im_frame): + if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): + delta = ImageChops.subtract_modulo(im_frame, base_im) + else: + delta = ImageChops.subtract_modulo( + im_frame.convert("RGB"), base_im.convert("RGB") + ) + return delta.getbbox() + + def _write_multiple_frames(im, fp, palette): duration = im.encoderinfo.get("duration") @@ -598,6 +608,12 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] + bbox = _getbbox(previous["im"], im_frame) + if not bbox: + # This frame is identical to the previous frame + if duration: + previous["encoderinfo"]["duration"] += encoderinfo["duration"] + continue if encoderinfo.get("disposal") == 2: if background_im is None: color = im.encoderinfo.get( @@ -606,21 +622,7 @@ def _write_multiple_frames(im, fp, palette): background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) background_im.putpalette(im_frames[0]["im"].palette) - base_im = background_im - else: - base_im = previous["im"] - if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im): - delta = ImageChops.subtract_modulo(im_frame, base_im) - else: - delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") - ) - bbox = delta.getbbox() - if not bbox: - # This frame is identical to the previous frame - if duration: - previous["encoderinfo"]["duration"] += encoderinfo["duration"] - continue + bbox = _getbbox(background_im, im_frame) else: bbox = None im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) From 7436ae0933ea4897111d2aebfb16b59a5c960a35 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Dec 2022 16:58:04 +0200 Subject: [PATCH 113/727] Remove unnecessary Pipfile --- MANIFEST.in | 2 - Pipfile | 22 ---- Pipfile.lock | 324 --------------------------------------------------- 3 files changed, 348 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock diff --git a/MANIFEST.in b/MANIFEST.in index 08f6dfc0877..f51551303f6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include *.c include *.h include *.in -include *.lock include *.md include *.py include *.rst @@ -10,7 +9,6 @@ include *.txt include *.yaml include LICENSE include Makefile -include Pipfile include tox.ini graft Tests graft src diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 1e611a63ce7..00000000000 --- a/Pipfile +++ /dev/null @@ -1,22 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -black = "*" -check-manifest = "*" -coverage = "*" -defusedxml = "*" -packaging = "*" -markdown2 = "*" -olefile = "*" -pyroma = "*" -pytest = "*" -pytest-cov = "*" -pytest-timeout = "*" - -[dev-packages] - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 600b19050f5..00000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,324 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "attrs": { - "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" - }, - "black": { - "hashes": [ - "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3", - "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f" - ], - "index": "pypi", - "version": "==21.12b0" - }, - "build": { - "hashes": [ - "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f", - "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "charset-normalizer": { - "hashes": [ - "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", - "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" - ], - "markers": "python_version >= '3'", - "version": "==2.0.9" - }, - "check-manifest": { - "hashes": [ - "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95", - "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce" - ], - "index": "pypi", - "version": "==0.47" - }, - "click": { - "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" - ], - "markers": "python_version >= '3.6'", - "version": "==8.0.3" - }, - "coverage": { - "hashes": [ - "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", - "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", - "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", - "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", - "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", - "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", - "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", - "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", - "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", - "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", - "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", - "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", - "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", - "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", - "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", - "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", - "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", - "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", - "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", - "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", - "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", - "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", - "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", - "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", - "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", - "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", - "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", - "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", - "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", - "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", - "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", - "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", - "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", - "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", - "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", - "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", - "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", - "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", - "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", - "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", - "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", - "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", - "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", - "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", - "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", - "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", - "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" - ], - "index": "pypi", - "version": "==6.2" - }, - "defusedxml": { - "hashes": [ - "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" - ], - "index": "pypi", - "version": "==0.7.1" - }, - "docutils": { - "hashes": [ - "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", - "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.18.1" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3'", - "version": "==3.3" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "markdown2": { - "hashes": [ - "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0", - "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e" - ], - "index": "pypi", - "version": "==2.4.2" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "olefile": { - "hashes": [ - "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" - ], - "index": "pypi", - "version": "==0.46" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "index": "pypi", - "version": "==21.3" - }, - "pathspec": { - "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" - ], - "version": "==0.9.0" - }, - "pep517": { - "hashes": [ - "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0", - "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161" - ], - "version": "==0.12.0" - }, - "platformdirs": { - "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" - ], - "markers": "python_version >= '3.6'", - "version": "==2.4.0" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pygments": { - "hashes": [ - "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", - "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" - ], - "markers": "python_version >= '3.5'", - "version": "==2.10.0" - }, - "pyparsing": { - "hashes": [ - "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", - "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.6" - }, - "pyroma": { - "hashes": [ - "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65", - "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a" - ], - "index": "pypi", - "version": "==3.2" - }, - "pytest": { - "hashes": [ - "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", - "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" - ], - "index": "pypi", - "version": "==6.2.5" - }, - "pytest-cov": { - "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" - ], - "index": "pypi", - "version": "==3.0.0" - }, - "pytest-timeout": { - "hashes": [ - "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112", - "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717" - ], - "index": "pypi", - "version": "==2.0.2" - }, - "requests": { - "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.26.0" - }, - "setuptools": { - "hashes": [ - "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c", - "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b" - ], - "markers": "python_version >= '3.7'", - "version": "==60.0.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", - "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.3" - }, - "typing-extensions": { - "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "urllib3": { - "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.7" - } - }, - "develop": {} -} From 66f5ad0eae90b6f4b07df1a3154f996c6fe00069 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Dec 2022 10:45:09 +1100 Subject: [PATCH 114/727] Ignore non-opaque WebP background when saving as GIF --- Tests/test_file_gif.py | 13 +++++++++++-- src/PIL/GifImagePlugin.py | 15 +++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 926f5c1eea8..a196c1612e8 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -859,14 +859,23 @@ def test_background(tmp_path): im.info["background"] = 1 im.save(out) with Image.open(out) as reread: - assert reread.info["background"] == im.info["background"] + +def test_webp_background(tmp_path): + out = str(tmp_path / "temp.gif") + + # Test opaque WebP background if features.check("webp") and features.check("webp_anim"): with Image.open("Tests/images/hopper.webp") as im: - assert isinstance(im.info["background"], tuple) + assert im.info["background"] == (255, 255, 255, 255) im.save(out) + # Test non-opaque WebP background + im = Image.new("L", (100, 100), "#000") + im.info["background"] = (0, 0, 0, 0) + im.save(out) + def test_comment(tmp_path): with Image.open(TEST_GIF) as im: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dd1b21f2e63..01518b378e6 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -886,20 +886,23 @@ def _get_palette_bytes(im): def _get_background(im, info_background): background = 0 if info_background: - background = info_background - if isinstance(background, tuple): + if isinstance(info_background, tuple): # WebPImagePlugin stores an RGBA value in info["background"] # So it must be converted to the same format as GifImagePlugin's # info["background"] - a global color table index try: - background = im.palette.getcolor(background, im) + background = im.palette.getcolor(info_background, im) except ValueError as e: - if str(e) == "cannot allocate more than 256 colors": + if str(e) not in ( # If all 256 colors are in use, # then there is no need for the background color - return 0 - else: + "cannot allocate more than 256 colors", + # Ignore non-opaque WebP background + "cannot add non-opaque RGBA color to RGB palette", + ): raise + else: + background = info_background return background From 4f0b83cc54230728bbd3593a3116cac046c5ee4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Dec 2022 12:29:27 +1100 Subject: [PATCH 115/727] Only set tile in ImageFile __setstate__ --- src/PIL/Image.py | 1 - src/PIL/ImageFile.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c2481b..bf93917ed0b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -704,7 +704,6 @@ def __getstate__(self): def __setstate__(self, state): Image.__init__(self) - self.tile = [] info, mode, size, palette, data = state self.info = info self.mode = mode diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f281b9e14c4..dbdc0cb38ce 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -137,6 +137,10 @@ def get_format_mimetype(self): if self.format is not None: return Image.MIME.get(self.format.upper()) + def __setstate__(self, state): + self.tile = [] + super().__setstate__(state) + def verify(self): """Check file integrity""" From ae3f43de64afbd59fdc424f37f18964dd25765e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Dec 2022 19:48:07 +1100 Subject: [PATCH 116/727] Document Hue range --- docs/handbook/concepts.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index a9b33e437e3..083351eec3b 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -41,6 +41,9 @@ supports the following standard modes: * ``LAB`` (3x8-bit pixels, the L*a*b color space) * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) + + * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees + * ``I`` (32-bit signed integer pixels) * ``F`` (32-bit floating point pixels) From f6f622dceee19fef36e6746a7943f2e806d8cabd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Dec 2022 06:36:27 +1100 Subject: [PATCH 117/727] 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 7faf0c2481b..155a546c29a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1482,7 +1482,8 @@ def getpalette(self, rawmode="RGB"): 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 164311a7568c7fed3c7a1dd60570cc182d3d5a0c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Dec 2022 06:55:10 +1100 Subject: [PATCH 118/727] Specify "I" and "F" ranges --- docs/handbook/concepts.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 083351eec3b..f3fa1f2b1bb 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -24,9 +24,10 @@ To get the number and names of bands in an image, use the Modes ----- -The ``mode`` of an image is a string which defines the type and depth of a pixel in the image. -Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range -of 0-1, an 8-bit pixel has a range of 0-255 and so on. The current release +The ``mode`` of an image is a string which defines the type and depth of a pixel in the +image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range of +0-1, an 8-bit pixel has a range of 0-255, a 32-signed integer pixel has the range of +INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release supports the following standard modes: * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) From 6da4169f3724ffe20c72d8ef4a2e0dc21815b343 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Dec 2022 22:40:55 +1100 Subject: [PATCH 119/727] 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 b90dde3d96c..48c0273fe46 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 ab9ac5ea23a..791e692c106 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -732,6 +732,8 @@ def load_string(self, data, legacy_api=True): @_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 1f9754cdc0c03405bbe1e3aa73b5dbb6750aa608 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:04:30 +0200 Subject: [PATCH 120/727] Format tox.ini with tox-ini-fmt --- .pre-commit-config.yaml | 9 +++++++-- tox.ini | 24 ++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d44874bf7cf..8d133b18dd3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black args: ["--target-version", "py37"] @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.11.1 hooks: - id: isort @@ -48,5 +48,10 @@ repos: hooks: - id: sphinx-lint + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 0.5.2 + hooks: + - id: tox-ini-fmt + ci: autoupdate_schedule: monthly diff --git a/tox.ini b/tox.ini index 21b5d4b506d..195522ffa9a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,13 @@ -# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, -# "python3 -m pip install tox" and then run "tox" from this directory. - [tox] envlist = lint - py{37,38,39,310,311,py3} + py{py3, 311, 310, 39, 38, 37} minversion = 1.9 [testenv] +deps = + cffi + numpy extras = tests commands = @@ -17,16 +15,14 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -deps = - cffi - numpy [testenv:lint] -commands = - pre-commit run --all-files --show-diff-on-failure - check-manifest +passenv = + PRE_COMMIT_COLOR +skip_install = true deps = + check-manifest pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure check-manifest -skip_install = true -passenv = PRE_COMMIT_COLOR From bfa1f3290c8ae830e0240dbfad2626fa6b49bb1b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:06:58 +0200 Subject: [PATCH 121/727] Add allowlist_externals=make to fix tox 4 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 195522ffa9a..9a41ca96b74 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} +allowlist_externals = make [testenv:lint] passenv = From 56964da7487c7fff897cd3b41f11d62922f84046 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 06:45:57 +1100 Subject: [PATCH 122/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f3ad8c797ed..1bcb9d2e90f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed bug combining GIF frame durations #6779 + [radarhere] + - Support saving JPEG comments #6774 [smason, radarhere] From 5301b86f1cd255fc55a464b38af176f37f91c396 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 06:48:36 +1100 Subject: [PATCH 123/727] Use snake case --- 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 e568e6afa0b..c2216e27ad0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1476,18 +1476,18 @@ def get_child_images(self): offset = current_offset fp = self.fp - thumbnailOffset = ifd.get(513) - if thumbnailOffset is not None: + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: try: - thumbnailOffset += self._exif_offset + thumbnail_offset += self._exif_offset except AttributeError: pass - self.fp.seek(thumbnailOffset) + self.fp.seek(thumbnail_offset) data = self.fp.read(ifd.get(514)) fp = io.BytesIO(data) with open(fp) as im: - if thumbnailOffset is None: + if thumbnail_offset is None: im._frame_pos = [ifd_offset] im._seek(0) im.load() From b564f3e6bf82bb705ae410b44422aefbc56198e2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 07:41:39 +1100 Subject: [PATCH 124/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1bcb9d2e90f..0372b5b378d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added IFD enum to ExifTags #6748 + [radarhere] + - Fixed bug combining GIF frame durations #6779 [radarhere] From e25d6031891cd53917379bb489d7b22614fe06fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Dec 2022 09:48:46 +1100 Subject: [PATCH 125/727] Updated xz to 5.4.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 66e352c73c8..0c3152b06a2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ def cmd_msbuild( "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.2.9.tar.gz/download", - "filename": "xz-5.2.9.tar.gz", - "dir": "xz-5.2.9", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download", + "filename": "xz-5.4.0.tar.gz", + "dir": "xz-5.4.0", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From d1cb81976cba7fbd3b13525a26b163cc42f029a7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 13 Dec 2022 18:32:55 +0200 Subject: [PATCH 126/727] Run Bandit on CI via pre-commit --- .pre-commit-config.yaml | 9 ++++++++- src/PIL/ImageShow.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d133b18dd3..609352f2217 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 22.12.0 hooks: - id: black - args: ["--target-version", "py37"] + args: [--target-version=py37] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] @@ -13,6 +13,13 @@ repos: hooks: - id: isort + - repo: https://github.com/PyCQA/bandit + rev: 1.7.4 + hooks: + - id: bandit + args: [--severity-level=high] + files: ^src/ + - repo: https://github.com/asottile/yesqa rev: v1.4.0 hooks: diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 76f42a3072d..9d52245889e 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -125,7 +125,7 @@ def show_file(self, path=None, **options): path = options.pop("file") else: raise TypeError("Missing required argument: 'path'") - os.system(self.get_command(path, **options)) + os.system(self.get_command(path, **options)) # nosec return 1 From 1a051f2e079253c74918ac89cbe899f4c6136bc3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Dec 2022 07:50:40 +0000 Subject: [PATCH 127/727] Update egor-tensin/cleanup-path action to v3 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 37dc694c6ca..f297eb1b56c 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -48,7 +48,7 @@ jobs: qt5-devel-tools subversion xorg-server-extra zlib-devel - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v2 + uses: egor-tensin/cleanup-path@v3 with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' From 7f6fe3c28728f0e68dba58b6ea9843de0b00ca3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Dec 2022 08:15:32 +1100 Subject: [PATCH 128/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0372b5b378d..1e5f71b865e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767 + [radarhere] + - Added IFD enum to ExifTags #6748 [radarhere] From 5eaca52efd86e41dc068802fd2683d433a45003e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Dec 2022 07:04:05 +1100 Subject: [PATCH 129/727] Updated harfbuzz to 6.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 0c3152b06a2..a1908e35ed9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -355,9 +355,9 @@ def cmd_msbuild( "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.1.zip", - "filename": "harfbuzz-5.3.1.zip", - "dir": "harfbuzz-5.3.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", + "filename": "harfbuzz-6.0.0.zip", + "dir": "harfbuzz-6.0.0", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From 2a86d7353f1a1435d564d0ac882268c80fc9486d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Dec 2022 08:19:15 +1100 Subject: [PATCH 130/727] Always initialize all plugins in registered_extensions() --- Tests/test_image.py | 6 ------ src/PIL/Image.py | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index b4e81e46613..69a66b85a71 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -401,8 +401,6 @@ def test_alpha_inplace(self): def test_registered_extensions_uninitialized(self): # Arrange Image._initialized = 0 - extension = Image.EXTENSION - Image.EXTENSION = {} # Act Image.registered_extensions() @@ -410,10 +408,6 @@ def test_registered_extensions_uninitialized(self): # Assert assert Image._initialized == 2 - # Restore the original state and assert - Image.EXTENSION = extension - assert Image.EXTENSION - def test_registered_extensions(self): # Arrange # Open an image to trigger plugin registration diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c2216e27ad0..6288f46ef30 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3418,8 +3418,7 @@ def registered_extensions(): Returns a dictionary containing all file extensions belonging to registered plugins """ - if not EXTENSION: - init() + init() return EXTENSION From 88e127d1b27f6dfbc6ddfe4ffba13748e89f4d11 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 22:16:07 +0000 Subject: [PATCH 131/727] Update actions/stale action to v7 --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ffac91ceca7..8c210bc9096 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v6 + uses: actions/stale@v7 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" From a065e0252b563b4d7a490ddd1a1eb7c8089662c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 09:29:25 +1100 Subject: [PATCH 132/727] Updated deprecated NumPy alias --- Tests/test_numpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 185e477ecc5..3de7ec30f5c 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -34,7 +34,7 @@ def to_image(dtype, bands=1, boolean=0): # Check supported 1-bit integer formats assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE) # Check supported 8-bit integer formats assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) @@ -193,7 +193,7 @@ def test_putdata(): "dtype", ( bool, - numpy.bool8, + numpy.bool_, numpy.int8, numpy.int16, numpy.int32, From d6e79045280be42cf2273716b18d82661cf7f779 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 12:47:46 +1100 Subject: [PATCH 133/727] Removed Python 3.7 on Cygwin --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index f297eb1b56c..7b8070d3477 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-minor-version: [7, 8, 9] + python-minor-version: [8, 9] timeout-minutes: 40 From 967034356a72d02e4cddad5ac4b6c75299d08394 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Dec 2022 14:20:47 +1100 Subject: [PATCH 134/727] Fixed BytesWarning --- src/PIL/PpmImagePlugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 392771d3e96..1670d9d64c5 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -208,7 +208,9 @@ def _decode_bitonal(self): tokens = b"".join(block.split()) for token in tokens: if token not in (48, 49): - raise ValueError(f"Invalid token for this mode: {bytes([token])}") + raise ValueError( + b"Invalid token for this mode: %s" % bytes([token]) + ) data = (data + tokens)[:total_bytes] invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) @@ -242,13 +244,13 @@ def _decode_blocks(self, maxval): half_token = tokens.pop() # save half token for later if len(half_token) > max_len: # prevent buildup of half_token raise ValueError( - f"Token too long found in data: {half_token[:max_len + 1]}" + b"Token too long found in data: %s" % half_token[: max_len + 1] ) for token in tokens: if len(token) > max_len: raise ValueError( - f"Token too long found in data: {token[:max_len + 1]}" + b"Token too long found in data: %s" % token[: max_len + 1] ) value = int(token) if value > maxval: From 1df7e75205247ae3ef021a623659d532bd5a4f15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 06:52:06 +1100 Subject: [PATCH 135/727] Python 3.7 on Cygwin is no longer part of CI --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index b559c824d11..b188020b993 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -460,7 +460,7 @@ These platforms are built and tested for every change. | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ -| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 | +| | 3.8, 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ From a4ac40354916401063028fc9af402e830eaf8606 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 07:14:02 +1100 Subject: [PATCH 136/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1e5f71b865e..04b3fc4c698 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Ignore non-opaque WebP background when saving as GIF #6792 + [radarhere] + +- Only set tile in ImageFile __setstate__ #6793 + [radarhere] + - When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767 [radarhere] From 9898613c4d276f8065d5e3bbb5fda7f4715e90d0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Dec 2022 15:31:36 +1100 Subject: [PATCH 137/727] Fixed saving EXIF data to MPO --- Tests/test_file_mpo.py | 5 ++++- src/PIL/MpoImagePlugin.py | 14 +++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index dba1ec1b11c..3e54762227a 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -80,7 +80,10 @@ def test_app(test_file): @pytest.mark.parametrize("test_file", test_files) def test_exif(test_file): - with Image.open(test_file) as im: + with Image.open(test_file) as im_original: + im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) + + for im in (im_original, im_reloaded): info = im._getexif() assert info[272] == "Nintendo 3DS" assert info[296] == 2 diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 3ae4d4abf5b..095cfe7ee96 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -52,14 +52,22 @@ def _save_all(im, fp, filename): _save(im, fp, filename) return + mpf_offset = 28 offsets = [] for imSequence in itertools.chain([im], append_images): for im_frame in ImageSequence.Iterator(imSequence): if not offsets: # APP2 marker - im.encoderinfo["extra"] = ( + im_frame.encoderinfo["extra"] = ( b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) + exif = im_frame.encoderinfo.get("exif") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif + if exif: + mpf_offset += 4 + len(exif) + JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) else: @@ -79,11 +87,11 @@ def _save_all(im, fp, filename): mptype = 0x000000 # Undefined mpentries += struct.pack(" Date: Thu, 22 Dec 2022 17:16:52 +1100 Subject: [PATCH 138/727] Initialize unsigned char variables --- src/libImaging/Quant.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index dfa6d842d3f..783852c24cf 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1717,7 +1717,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { withAlpha = !strcmp(im->mode, "RGBA"); int transparency = 0; - unsigned char r, g, b; + unsigned char r = 0, g = 0, b = 0; for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; From 88f15eb9f07b0434a6b2831b02d402dd4efdee6c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Dec 2022 12:10:36 +1100 Subject: [PATCH 139/727] Do not save EXIF from info --- Tests/test_file_png.py | 12 ++++++++++-- src/PIL/PngImagePlugin.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 37235fe6f02..9481cd5ddfe 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -706,10 +706,18 @@ def test_exif(self): assert exif[274] == 3 def test_exif_save(self, tmp_path): + # Test exif is not saved from info + test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/exif.png") as im: - test_file = str(tmp_path / "temp.png") im.save(test_file) + with Image.open(test_file) as reloaded: + assert reloaded._getexif() is None + + # Test passing in exif + with Image.open("Tests/images/exif.png") as im: + im.save(test_file, exif=im.getexif()) + with Image.open(test_file) as reloaded: exif = reloaded._getexif() assert exif[274] == 1 @@ -720,7 +728,7 @@ def test_exif_save(self, tmp_path): def test_exif_from_jpg(self, tmp_path): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") - im.save(test_file) + im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: exif = reloaded._getexif() diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 2c53be109ae..b6a3c4cb660 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1383,7 +1383,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): chunks.remove(cid) chunk(fp, cid, data) - exif = im.encoderinfo.get("exif", im.info.get("exif")) + exif = im.encoderinfo.get("exif") if exif: if isinstance(exif, Image.Exif): exif = exif.tobytes(8) From 9e6a7d974084a4d7b6be9d68b732558194d20e51 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Dec 2022 17:43:18 +1100 Subject: [PATCH 140/727] Added support for uncompressed L images --- Tests/images/uncompressed_l.dds | Bin 0 -> 16512 bytes Tests/images/uncompressed_l.png | Bin 0 -> 861 bytes Tests/test_file_dds.py | 12 +++++++++-- src/PIL/DdsImagePlugin.py | 35 ++++++++++++++++++++++++-------- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 Tests/images/uncompressed_l.dds create mode 100644 Tests/images/uncompressed_l.png diff --git a/Tests/images/uncompressed_l.dds b/Tests/images/uncompressed_l.dds new file mode 100644 index 0000000000000000000000000000000000000000..b82282587ec30665fceafced278f1c59b3402ed1 GIT binary patch literal 16512 zcmeH}-EHGA5QLRH-P>I{2-3o};0}Uxa}TK}#ia!we>Mbmxid@IFa*O(Adcw~@eMx; zf=;LR*MHl#{rikdie4wCtD@J5$rZg$Odd=pyeTl@O@Rr&tAImS3LLsp;L!Id0QjK*;D-W$ zUsiB1AL2y-+`b5a+g}qv@T~yBw*myeRN!Df#TNl|`$YiV{(^u9=Lg$A2l~GQ{ow&5 zpBqU3+`zrxe;YskeE#v{zyA5p51_m@(gG!?cVO_^Sz~$wl>F9wR-n}<1zJu7v^@NP z24p2HAUP}$lKTZm^U(>6`arZRRKVo%R5fre zR}Gw8HE{BUpFaA4SDyNS`QJWZP6L6%3Ic}}1b$%!MXwXnRnhCjXRluPu1rA*)aOis!0Q^t@@IwK>FDp2h5Ah-ZZeIkz?XL+S_*Q`6TLFS!DsV8L z;)?*f{UU&Fe?h>5^Mmbw1o}^b{_p^j&kdx1Zs6YUzl|S%KL7afU;q5)2T)!cX@Qc{ zJ1}_dtTDX-N`7lTE70nb0xc&3S{{Bt1F{kpkQ^2W$^C+(`RD{jeIQ`uK)}f3FOCAW z$Z4Q*r-91D$yC6smsv0<#s|HT48aVmFPj7v|D_{D6`BNV-r-8s>1%bl~ z0>7|=qSuM(s_1oMaz(EblLylYZwgF!Q((gHD&WwS0*9^?IP^UV0DdR{_@MycmlYh$ zhjsv0<#s|HT4 t8aVmFPf`J1DHUKY6=0r08b||aAPuB}G>`_;KpIE`X&?=xfi!T4f&Vw%wVD6` literal 0 HcmV?d00001 diff --git a/Tests/images/uncompressed_l.png b/Tests/images/uncompressed_l.png new file mode 100644 index 0000000000000000000000000000000000000000..9d22a26a446d3dbdfd8f9c931ea466f6c6424e90 GIT binary patch literal 861 zcmeAS@N?(olHy`uVBq!ia0vp^4Is<`Bp9BB+KDqTFspdFIEGZrc^h?ls*<5Vur)JB zqlg-7P(pKyh(}`Z41qR*8_gT%*K1u(y=E67zCX-RIr5$RbYFRmk9~(3o90{0iTo?q zZokIPVc}P`n#F#$cTRI}Z`b*cHdxb-Qkj7%xN9XC&Z5~v}sUc zQF_LyDDi8i`mdSklfSkId~vT6XS8VuNn~8YHGhS2Q^L#RQrG)UyL@3X&}x&GB>;Tib{;+mb; z9E?oS*}aKX$Uxa5;eK2CjmcMXGaH^hIH47DKd5laQEeT@eua-ZO-D9&R()gjOuir% zvW=}bGBx``)z5EfnVi1_vX=2LOsg_|ey?EeT|36sB|Uh!dQU<13AVi4r5g)!U-!1I-}Y|HguFw3`xCAHw!QqqenxjbTuMSUDiOXag;?62vFzD;OU-N3|GNKs$H|s>F&hgWVD=Cdb6BTa*mH8 zSvtelvqpRLzjllcxGA-Tb?VxK$@jOLdwX91dKc*H#c{mLBht%bs(#FW&Mb`zE`Q|r X-moxli&n>DP`>eW^>bP0l+XkK*RXjv literal 0 HcmV?d00001 diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 4b9f8949ef5..f579cd1c257 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -22,6 +22,7 @@ TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" +TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" @@ -194,8 +195,14 @@ def test_unimplemented_dxgi_format(): pass -def test_uncompressed_rgb(): - """Check uncompressed RGB images can be opened""" +def test_uncompressed(): + """Check uncompressed images can be opened""" + with Image.open(TEST_FILE_UNCOMPRESSED_L) as im: + assert im.format == "DDS" + assert im.mode == "L" + assert im.size == (128, 128) + + assert_image_equal_tofile(im, "Tests/images/uncompressed_l.png") # convert -format dds -define dds:compression=none hopper.jpg hopper.dds with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: @@ -305,6 +312,7 @@ def test_save_unsupported_mode(tmp_path): @pytest.mark.parametrize( ("mode", "test_file"), [ + ("L", "Tests/images/linear_gradient.png"), ("RGB", "Tests/images/hopper.png"), ("RGBA", "Tests/images/pil123rgba.png"), ], diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index eea6e31534c..b78cc649f13 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -135,7 +135,12 @@ def _open(self): fourcc = header.read(4) (bitcount,) = struct.unpack(" Date: Mon, 8 Aug 2022 02:24:55 +0300 Subject: [PATCH 141/727] Add missing LA test textures --- Tests/images/la.dds | Bin 0 -> 32896 bytes Tests/images/la.png | Bin 0 -> 1060 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Tests/images/la.dds create mode 100644 Tests/images/la.png diff --git a/Tests/images/la.dds b/Tests/images/la.dds new file mode 100644 index 0000000000000000000000000000000000000000..30bf93576fd17f80397a1a016c3ee2306e4bf28a GIT binary patch literal 32896 zcmeI4;f>T#420j&0MG%V1zNZc5Z&;DdMJ(-j@^xtFtTUtJV%OdaU|H-`0+4X(6pEBpi3mQLUN zhAVA5p%B0!$UPjuA;|dw9Djv6iQ(a(_u$>I1Ojy6clnLvvWhAUCVm}G%is+ZHu896>c61SC68$csOrpOQNl+^8 zJtZg=cfSOs;_k2AQNU1G`clA9So{<)6l&b7JH(^~&%jUNQoik;evw#x!ROi zq3`pbTr({T)*H&H70aUD|CJNy-#_6Imjze<{;7TgzW@DKCa}DDa=}tu{POEfinTX? zb<;2RP!+ZuVEt!g0Fef~lfiTo-0z~AB=^@93cU%shYJiz?KiL{q5Ws5 z8>+dGpL4Ya?ey1*vMd$_D29ehEs&-Cw(-fT6JTrGTNZ_$gp0 z)VNo7h)D~cfuF*qgfYS;%Yre&CCh?!hx4B)Oj^|Za=oE=_5SH%lR^j>6v)G!66a6; z-I>IsWdU9&rx0aP^H(N77n>Bq1qTIkwJEVe-{(KMW?B}kH^rdg<&vqm`|Ee% zs!aj*l*il^(N7_%fc|P^@WKRkbP;6|{S=c5=&wg1FcjE+64r>;PeEP5`p?J!A`N&a zgXt!?-$gY^?yoBpdJ}XH7Z{S-Z(vPA`_E7}RCAMiI^aT(^Bd$Xk@KHZ^%bt;&-@J6 zasLZk$NlpT0EZy=OaO-<=Lc{Ia{fvc;JSf+I^ep2_5-dPXn#$0flZS4aDh#d`(0p@ z<>kY-L_fHp_6hgqDpmg{j=J}Ih@nI|F6h(W=Tw17T|?)^H(RZZgTdMt~Mp~@1O99%Yv(a|5Segy#M`ICa}D7a=}tu z{PW)cv+q3mQZKq&^c?)NCt#nVTrw4R|J(`K{q;L{y)ubCDI^upe|!SyuSOEp6|p15 zqyqY{=0Exl;J^MJQBYT~{$mrc{xdS}qD_)_a#2l^`*Snk{<>mdO+xoDuqL7X@dUL0 z4AmfSiQLl-@|MW?`3!RYbE-Dz$IbC)ZqSdL<9?jNaeq5{0zHAAKu@42&=cqh^aOeW tJ%OG;PoO8z6X*%_1bPBJfu2B5peN81=n3=$dICLxow)~&Z0 zH5EtM_}-^3Hu{@?P>lS=P97Ps&2Y`uaavk;f+X z|NQ$jeEDRv|F65_YwNN;r`k*j^g*GU zZrQreW}1D!B^c{>PdvO!Q}DcywYc$d-YXYC%pI{Ec)v#0Q1dpzO) z>}bXbHv4f(YB%FVzfuC*mB;y3?5j z1Y_6l`*Zr}^!K%%U%v1;Fm4i`z{@pZZ$nN1!{0UQ-Rxhl{Qn3%@>4BHFG zvO)N_WU1bT6BB&dj~$)VdmKdUP~ty6OIP9JK^?b8&5=DWQEzT4__ZJBudVs8`g7#( zEAQX`@v{F_RsA^p{{34ulKk8O-p@~*f)PiiFVsE|QIi7_fuGYeL8Um$eIo#hy0rW-j^kyJGGiySUreHRK;G{qW88_J1>XAU=NS zyafLm7qg!>=auF^k6h|9g(3RDz4}GE*KdJ{%kKNxA2hsSP1#&w#xU{!KZg8$-&HSO zwq{@j1*3+;H>ORFj9)hIlX{<mc)r>dSOl$mp?&q7^EYrKsANhAsLdGHG zJ20rK7CedkEnz6J*YW@;s5BYRZ}jM#$(_tSC4Y-t#^bFZ{I|zSJfrVe%H_twjVX^e z0tw}HCk}5+m~ShZQl<%b`g*RTKc>bd*xEBgAN@LH9B?8Cq8^B=a` z{7TfFtE}w|A`btsoASe`WPg^sa;d+23pTn#+UJ zi>uD)Er=RDdV4Fvzpy&-gjnM+dDD|ps Date: Fri, 23 Dec 2022 19:07:45 +1100 Subject: [PATCH 142/727] Added support for uncompressed LA images --- Tests/images/{la.dds => uncompressed_la.dds} | Bin Tests/images/{la.png => uncompressed_la.png} | Bin Tests/test_file_dds.py | 38 ++++++++----------- src/PIL/DdsImagePlugin.py | 31 ++++++++------- 4 files changed, 33 insertions(+), 36 deletions(-) rename Tests/images/{la.dds => uncompressed_la.dds} (100%) rename Tests/images/{la.png => uncompressed_la.png} (100%) diff --git a/Tests/images/la.dds b/Tests/images/uncompressed_la.dds similarity index 100% rename from Tests/images/la.dds rename to Tests/images/uncompressed_la.dds diff --git a/Tests/images/la.png b/Tests/images/uncompressed_la.png similarity index 100% rename from Tests/images/la.png rename to Tests/images/uncompressed_la.png diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index f579cd1c257..cac4108a8f0 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -23,6 +23,7 @@ TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" +TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" @@ -195,32 +196,24 @@ def test_unimplemented_dxgi_format(): pass -def test_uncompressed(): +@pytest.mark.parametrize( + ("mode", "size", "test_file"), + [ + ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), + ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), + ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB), + ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), + ], +) +def test_uncompressed(mode, size, test_file): """Check uncompressed images can be opened""" - with Image.open(TEST_FILE_UNCOMPRESSED_L) as im: - assert im.format == "DDS" - assert im.mode == "L" - assert im.size == (128, 128) - - assert_image_equal_tofile(im, "Tests/images/uncompressed_l.png") - - # convert -format dds -define dds:compression=none hopper.jpg hopper.dds - with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: - assert im.format == "DDS" - assert im.mode == "RGB" - assert im.size == (128, 128) - assert_image_equal_tofile(im, "Tests/images/hopper.png") - - # Test image with alpha - with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im: + with Image.open(test_file) as im: assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (800, 600) + assert im.mode == mode + assert im.size == size - assert_image_equal_tofile( - im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png") - ) + assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) def test__accept_true(): @@ -313,6 +306,7 @@ def test_save_unsupported_mode(tmp_path): ("mode", "test_file"), [ ("L", "Tests/images/linear_gradient.png"), + ("LA", "Tests/images/uncompressed_la.png"), ("RGB", "Tests/images/hopper.png"), ("RGBA", "Tests/images/pil123rgba.png"), ], diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index b78cc649f13..f78c8b17cbd 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -136,15 +136,18 @@ def _open(self): (bitcount,) = struct.unpack(" Date: Fri, 23 Dec 2022 23:20:06 +1100 Subject: [PATCH 143/727] Clear pyaccess after re-assigning im --- Tests/test_file_ico.py | 13 +++++++++++++ src/PIL/IcoImagePlugin.py | 1 + 2 files changed, 14 insertions(+) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 3fcd5c61f0d..afb17b1afad 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -71,6 +71,19 @@ def test_save_to_bytes(): ) +def test_getpixel(tmp_path): + temp_file = str(tmp_path / "temp.ico") + + im = hopper() + im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) + + with Image.open(temp_file) as reloaded: + reloaded.load() + reloaded.size = (32, 32) + + assert reloaded.getpixel((0, 0)) == (18, 20, 62) + + def test_no_duplicates(tmp_path): temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 17b9855a0a5..93b9dfdea7b 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -327,6 +327,7 @@ def load(self): # if tile is PNG, it won't really be loaded yet im.load() self.im = im.im + self.pyaccess = None self.mode = im.mode if im.size != self.size: warnings.warn("Image was not the expected size") From 8bd5fbf450f9f5a03a32c7efcfb488bfc2a40d1c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 07:32:09 +1100 Subject: [PATCH 144/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 04b3fc4c698..3e409fe6417 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,24 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Fixed PyAccess after changing ICO size #6821 + [radarhere] + +- Do not use EXIF from info when saving PNG images #6819 + [radarhere] + +- Fixed saving EXIF data to MPO #6817 + [radarhere] + +- Added Exif hide_offsets() #6762 + [radarhere] + +- Only compare to previous frame when checking for duplicate GIF frames while saving #6787 + [radarhere] + +- Always initialize all plugins in registered_extensions() #6811 + [radarhere] + - Ignore non-opaque WebP background when saving as GIF #6792 [radarhere] From 5c482e20af8efe0210cd7b0cfe2dec7367d03042 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 08:32:58 +1100 Subject: [PATCH 145/727] Document new ExifTags enums --- docs/reference/ExifTags.rst | 10 +++++++++- docs/releasenotes/9.4.0.rst | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 650bb4f9546..464ab77ea35 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -37,7 +37,15 @@ which provide constants and clear-text names for various well-known EXIF tags. >>> IFD.Exif.value 34665 >>> IFD(34665).name - 'Exif' + 'Exif + +.. py:data:: LightSource + + >>> from PIL.ExifTags import LightSource + >>> LightSource.Unknown.value + 0 + >>> LightSource(0).name + 'Unknown' Two of these values are also exposed as dictionaries. diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ccbe62a6bcd..7da0e61f312 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -45,6 +45,34 @@ 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 IFD, Interop and LightSource ExifTags enums +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:data:`~PIL.ExifTags.IFD` has been added, allowing enums to be used with +:py:meth:`~PIL.Image.Exif.get_ifd`:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/flower.jpg") + print(im.getexif().get_ifd(ExifTags.IFD.Exif)) + +``IFD1`` can also be used with :py:meth:`~PIL.Image.Exif.get_ifd`, but it should +not be used in other contexts, as the enum value is only internally meaningful. + +:py:data:`~PIL.ExifTags.Interop` has been added for tags within the Interop IFD:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/flower.jpg") + interop_ifd = im.getexif().get_ifd(ExifTags.IFD.Interop) + print(interop_ifd.get(ExifTags.Interop.InteropIndex)) # R98 + +:py:data:`~PIL.ExifTags.LightSource` has been added for values within the LightSource +tag:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/iptc.jpg") + exif_ifd = im.getexif().get_ifd(ExifTags.IFD.Exif) + print(ExifTags.LightSource(exif_ifd[0x9208])) # LightSource.Unknown + getxmp() ^^^^^^^^ From 941a2d60b28c32f1193ef2f9627fc80f9279802d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 08:41:57 +1100 Subject: [PATCH 146/727] Added release notes --- docs/releasenotes/9.4.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index ccbe62a6bcd..0068f281608 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -70,7 +70,8 @@ TODO Other Changes ============= -TODO -^^^^ +Added support for DDS L and LA images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Support has been added to read and write L and LA DDS images in the uncompressed +format, known as "luminance" textures. From 426ac9c1fe085d78d501c7143039b83a23eeac3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Dec 2022 14:19:32 +1100 Subject: [PATCH 147/727] Updated libtiff to 4.5.0 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index b188020b993..42fe8c254fa 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -143,7 +143,7 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.4** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5** * **libfreetype** provides type related services diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a1908e35ed9..0b0c782a060 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -200,15 +200,11 @@ def cmd_msbuild( "libs": [r"output\release-static\{architecture}\lib\*.lib"], }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz", - "filename": "tiff-4.4.0.tar.gz", - "dir": "tiff-4.4.0", - "license": "COPYRIGHT", + "url": "https://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz", + "filename": "tiff-4.5.0.tar.gz", + "dir": "tiff-4.5.0", + "license": "LICENSE.md", "patch": { - r"cmake\LZMACodec.cmake": { - # fix typo - "${{LZMA_FOUND}}": "${{LIBLZMA_FOUND}}", - }, r"libtiff\tif_lzma.c": { # link against liblzma.lib "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 From d2590437c4f90b1f6837f951223e18923c4d3467 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 16:21:45 +1100 Subject: [PATCH 148/727] Updated libtiff shared library name --- Tests/oss-fuzz/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index b459ee47a55..7e9098f530b 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -25,7 +25,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do --add-binary /usr/local/lib/liblcms2.so.2:. \ --add-binary /usr/local/lib/libopenjp2.so.7:. \ --add-binary /usr/local/lib/libpng16.so.16:. \ - --add-binary /usr/local/lib/libtiff.so.5:. \ + --add-binary /usr/local/lib/libtiff.so.6:. \ --add-binary /usr/local/lib/libwebp.so.7:. \ --add-binary /usr/local/lib/libwebpdemux.so.2:. \ --add-binary /usr/local/lib/libwebpmux.so.3:. \ From 08816f43ae621830cd4cf9dc1fecfbae63e5cc60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 15:46:14 +1100 Subject: [PATCH 149/727] Added support for I;16 modes in putdata() --- Tests/test_image_putdata.py | 5 +++-- src/_imaging.c | 28 ++++++++++++---------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 3d60e52a294..0e6293349bc 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 940b5fbb39d..05e1370f6e3 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); + 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; + } + if (endian == 0) { image->image8[y][x] = (UINT8)CLIP8(value); - if (++x >= (int)image->xsize) { - x = 0, y++; - } + } 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); } - - } 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 (++x >= (int)image->xsize) { + x = 0, y++; } } PyErr_Clear(); /* Avoid weird exceptions */ From 2755e0ffaadc8b29c3e67e223c333c50e197a733 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Dec 2022 19:24:41 +1100 Subject: [PATCH 150/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3e409fe6417..76fc230a838 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.4.0 (unreleased) ------------------ +- Added DDS support for uncompressed L and LA images #6820 + [radarhere, REDxEYE] + +- Added LightSource tag values to ExifTags #6749 + [radarhere] + - Fixed PyAccess after changing ICO size #6821 [radarhere] From bcdb208fe2365b75c8d87d29783b9cc9f8cb683b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 09:44:53 +1100 Subject: [PATCH 151/727] 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 a37c90296eb..a0c50b5f407 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -921,12 +921,7 @@ def test_categories_deprecation(self): 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 @@ def test_constants_deprecation(self): 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 dec652df88f..bbd87380055 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 a760de575a6..a31ec380088 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 152/727] 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 f3fa1f2b1bb..f7bc9396b23 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 700a8e98da37cb586e15fffeb46c04d755950f7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 17:16:44 +1100 Subject: [PATCH 153/727] Support converting between I;16N and L --- Tests/test_mode_i16.py | 11 ++++------- src/libImaging/Convert.c | 7 +++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index efcdab9ec43..13487402572 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -90,10 +90,7 @@ def test_convert(): im = original.copy() - verify(im.convert("I;16")) - verify(im.convert("I;16").convert("L")) - verify(im.convert("I;16").convert("I")) - - verify(im.convert("I;16B")) - verify(im.convert("I;16B").convert("L")) - verify(im.convert("I;16B").convert("I")) + for mode in ("I;16", "I;16B", "I;16N"): + verify(im.convert(mode)) + verify(im.convert(mode).convert("L")) + verify(im.convert(mode).convert("I")) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 2b45d0cc4a8..8463e48965d 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -970,6 +970,13 @@ static struct { {"I;16L", "L", I16L_L}, {"L", "I;16B", L_I16B}, {"I;16B", "L", I16B_L}, +#ifdef WORDS_BIGENDIAN + {"L", "I;16N", L_I16B}, + {"I;16N", "L", I16B_L}, +#else + {"L", "I;16N", L_I16L}, + {"I;16N", "L", I16L_L}, +#endif {"I;16", "F", I16L_F}, {"I;16L", "F", I16L_F}, From e908e10aab5a40caec9988644640382d8e7d2631 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 17:25:28 +1100 Subject: [PATCH 154/727] Support packing I;16N --- Tests/test_lib_pack.py | 3 +++ src/libImaging/Pack.c | 1 + 2 files changed, 4 insertions(+) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 979806cae99..dc5bbb77bc5 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -207,6 +207,9 @@ def test_I(self): 0x01000083, ) + def test_I16(self): + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + def test_F_float(self): self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 01760e742be..14c8f1461aa 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -664,6 +664,7 @@ static struct { #endif {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, packI16N_I16}, {"I;16B", "I;16N", 16, packI16N_I16B}, From 8cfc25618f1f66d32dad778be92aac16e34bcc6d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 18:57:24 +1100 Subject: [PATCH 155/727] Support unpacking I;16N --- Tests/test_lib_pack.py | 2 ++ src/libImaging/Unpack.c | 1 + 2 files changed, 3 insertions(+) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index dc5bbb77bc5..de3e7d1569b 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -764,10 +764,12 @@ def test_I16(self): self.assert_unpack("I;16", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16B", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16L", "I;16N", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) else: self.assert_unpack("I;16", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) def test_CMYK16(self): self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e426ed74fce..7eeadf944ea 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1762,6 +1762,7 @@ static struct { {"I;16", "I;16", 16, copy2}, {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. From a0e1608f4fe9496dae98e3764969ae92a98ccdf5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 16:55:59 +1100 Subject: [PATCH 156/727] Support accessing I;16N pixels --- Tests/test_image_access.py | 24 +++++++----------------- src/PIL/PyAccess.py | 1 + src/libImaging/Access.c | 9 ++++++++- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 6c4f1ceec04..d13cff2211b 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -266,15 +266,10 @@ def test_get_vs_c(self): # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? self._test_get_access(hopper("F")) - im = Image.new("I;16", (10, 10), 40000) - self._test_get_access(im) - im = Image.new("I;16L", (10, 10), 40000) - self._test_get_access(im) - im = Image.new("I;16B", (10, 10), 40000) - self._test_get_access(im) - - im = Image.new("I", (10, 10), 40000) - self._test_get_access(im) + for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): + im = Image.new(mode, (10, 10), 40000) + self._test_get_access(im) + # These don't actually appear to be modes that I can actually make, # as unpack sets them directly into the I mode. # im = Image.new('I;32L', (10, 10), -2**10) @@ -313,15 +308,10 @@ def test_set_vs_c(self): # self._test_set_access(i, (128, 128)) #PA -- undone how to make self._test_set_access(hopper("F"), 1024.0) - im = Image.new("I;16", (10, 10), 40000) - self._test_set_access(im, 45000) - im = Image.new("I;16L", (10, 10), 40000) - self._test_set_access(im, 45000) - im = Image.new("I;16B", (10, 10), 40000) - self._test_set_access(im, 45000) + for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): + im = Image.new(mode, (10, 10), 40000) + self._test_set_access(im, 45000) - im = Image.new("I", (10, 10), 40000) - self._test_set_access(im, 45000) # im = Image.new('I;32L', (10, 10), -(2**10)) # self._test_set_access(im, -(2**13)+1) # im = Image.new('I;32B', (10, 10), 2**10) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 039f5ceeacf..2043b539cba 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -318,6 +318,7 @@ def set_pixel(self, x, y, color): "1": _PyAccess8, "L": _PyAccess8, "P": _PyAccess8, + "I;16N": _PyAccessI16_N, "LA": _PyAccess32_2, "La": _PyAccess32_2, "PA": _PyAccess32_2, diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 83860c38a7e..f00939da0b3 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -13,7 +13,7 @@ /* use make_hash.py from the pillow-scripts repository to calculate these values */ #define ACCESS_TABLE_SIZE 27 -#define ACCESS_TABLE_HASH 3078 +#define ACCESS_TABLE_HASH 33051 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; @@ -92,6 +92,12 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } +static void +get_pixel_16(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x + x]; + memcpy(color, in, sizeof(UINT16)); +} + static void get_pixel_32(Imaging im, int x, int y, void *color) { memcpy(color, &im->image32[y][x], sizeof(INT32)); @@ -186,6 +192,7 @@ ImagingAccessInit() { ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16B", get_pixel_16B, put_pixel_16B); + ADD("I;16N", get_pixel_16, put_pixel_16L); ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); From 0da8e43977f11837d9175419884d6a3295a7651e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Dec 2022 08:58:38 +1100 Subject: [PATCH 157/727] 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 48c0273fe46..48797ea084d 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 - out = str(tmp_path / "temp.tiff") - for (value, expected) in {b"test": "test", 1: "1"}.items(): - info[271] = value + info[271] = value - im.save(out, tiffinfo=info) + im = hopper() + out = str(tmp_path / "temp.tiff") + 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 158/727] 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 aae3e2b64bc..e4e1e40fed1 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 159/727] 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 01442dc69f0..317db4c0120 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -65,12 +65,12 @@ def test_grabclipboard(self): 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 160/727] 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 f7bc9396b23..ed25e186508 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 161/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 76fc230a838..cc6bb2e3e1b 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 162/727] 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 ed25e186508..01f75e9a35d 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 163/727] 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 bbd87380055..a84a9fe3654 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 48ce6fef70c..e97b58a41cd 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 aae3e2b64bc..e4c47401c5f 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 164/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cc6bb2e3e1b..aa0fa2a7463 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 165/727] 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 0a79b1237f3..f7b1ebd9f04 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3687,7 +3687,7 @@ def tobytes(self, offset=8): 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 166/727] 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 6ded944da7a..a061aaf1743 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -289,7 +289,7 @@ def cmd_msbuild( # "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 167/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa0fa2a7463..4eebbda6aa9 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 168/727] 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 609352f2217..d019d3e7fce 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 ec3938b367f..26451533eee 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 @@ def _bitmap(self, header=0, offset=0): # ---------------------- 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 @@ def _bitmap(self, header=0, offset=0): ): 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 @@ def _open(self): 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 9510f733e1a..a0da1b78622 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -42,7 +42,8 @@ def _open(self): 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 @@ def _load(self): 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 42af5cafcef..aedc6ce7f65 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -43,7 +43,8 @@ def _open(self): # 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 @@ def _open(self): 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 aeed1e7c7ba..81c0314f020 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -47,7 +47,8 @@ def _open(self): # 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 f78c8b17cbd..a946daeaa6b 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 @@ def _open(self): # 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 @@ def _open(self): 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 @@ def _open(self): 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 @@ def _find_offset(self, fp): 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 c16300efa89..536bc1fe695 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -28,7 +28,8 @@ def _open(self): 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 @@ def _open(self): 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 440240a9958..86eb2d5a204 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -67,7 +67,8 @@ def _load(self): 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 908bed9f427..66681939d9d 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -50,7 +50,8 @@ def _open(self): # 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 @@ def _seek(self, frame): 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 a55376d0e08..8ddc6b40be1 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -60,10 +60,12 @@ def _open(self): 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 @@ def _open_index(self, index=1): 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 @@ def _open_subimage(self, index=1, subimage=0): 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 @@ def _open_subimage(self, index=1, subimage=0): 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 1b714eb4f65..c7c32252b87 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 4575f8237dc..2088eb7b014 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -42,7 +42,8 @@ def _open(self): 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 @@ def _load(self): 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 df11cf2a6db..d6f2837393f 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -42,7 +42,8 @@ def _open(self): 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 @@ def _load(self): 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 fa192f053f9..e76d0c35a74 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 @@ def __init__(self, fobj): 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 @@ def itersizes(self): 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 @@ def size(self, value): 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 93b9dfdea7b..568e6d38db5 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -127,7 +127,8 @@ def __init__(self, buf): # 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 @@ def size(self): @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 31b0ff46901..d0e9508fed8 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -126,7 +126,8 @@ def _open(self): # 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 @@ def _open(self): 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 @@ def _open(self): 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 @@ def _open(self): ) 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 @@ def _open(self): 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 f7b1ebd9f04..386fb7c26ea 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 @@ class DecompressionBombError(Exception): 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 @@ def _repr_png_(self): 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 @@ def tobytes(self, encoder_name="raw", *args): 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 @@ def tobitmap(self, name="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 @@ def frombytes(self, data, decoder_name="raw", *args): 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 @@ def convert( 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 @@ def convert_transparency(m, v): 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 @@ def convert_transparency(m, v): 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 @@ def quantize( 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 @@ def crop(self, box=None): 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 @@ def filter(self, filter): 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 @@ def paste(self, im, box=None, mask=None): 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 @@ def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): """ 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 @@ def point(self, data): 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 @@ def putalpha(self, alpha): 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 @@ def putalpha(self, alpha): 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 @@ def putpalette(self, data, rawmode="RGB"): 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 @@ def remap_palette(self, dest_map, source_palette=None): 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 @@ def resize(self, size, resample=None, box=None, reducing_gap=None): ) 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 @@ def save(self, fp, format=None, **params): 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 @@ def getchannel(self, channel): 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 @@ def getdata(self): 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 @@ def __transformer( ) else: - raise ValueError("unknown transformation method") + msg = "unknown transformation method" + raise ValueError(msg) if resample not in ( Resampling.NEAREST, @@ -2791,7 +2821,8 @@ def toqimage(self): 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 @@ def toqpixmap(self): 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 605252d5d4c..2a2d372e52e 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 @@ def __init__(self, profile): 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 @@ def apply(self, im, imOut=None): 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 9cbce4143f3..e184ed68da3 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 40754423449..ce29a163b1b 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -69,7 +69,8 @@ def __init__(self, im, mode=None): 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 @@ def text( ) 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 @@ def multiline_text( 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 @@ def multiline_text( 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 @@ def textlength( ): """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 @@ def textbbox( ): """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 @@ def multiline_textbbox( 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 @@ def multiline_textbbox( 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 dbdc0cb38ce..0d3facf5716 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -124,7 +124,8 @@ def __init__(self, fp=None, filename=None): 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 @@ def load(self): """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 @@ def load(self): 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 @@ def _seek_check(self, frame): 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 @@ def load(self): 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 @@ def close(self): 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 @@ def setimage(self, im, extents=None): 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 @@ def set_as_raw(self, data, rawmode=None): 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 e10c6fdf14d..59e2c18b9ac 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 @@ def __init__(self, size, kernel, scale=None, offset=0): # 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 @@ def __init__(self, size, rank): 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 @@ def __init__(self, size, table, channels=3, target_mode=None, **kwargs): 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 @@ def _check_size(size): 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 @@ def generate(cls, size, callback, channels=3, target_mode=None): """ 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 @@ def transform(self, callback, with_normals=False, channels=None, target_mode=Non 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 3b1a2a23ab4..b144c3dd279 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 @@ def _load_pilfont(self, filename): 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 @@ def _load_pilfont_data(self, file, image): # 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 @@ def _load_pilfont_data(self, file, image): # 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 @@ def get_variation_names(self): 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 @@ def get_variation_axes(self): 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 @@ def set_variation_by_axes(self, axes): 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 @@ def getbbox(self, text, *args, **kwargs): 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 8cf95680995..982f77f206d 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 09d9898d750..ac7d36b698c 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -39,7 +39,8 @@ def __fixup(self, im1): 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 @@ def apply(self, op, im1, im2=None, mode=None): 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 @@ def apply(self, op, im1, im2=None, mode=None): 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 scan(code): 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 1e22c36a8ae..60cbbedc384 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -81,7 +81,8 @@ def __init__(self, patterns=None, op_name=None): ], } 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 @@ def apply(self, image): 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 @@ def match(self, image): 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 @@ def get_on_pixels(self, image): 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 @@ def load_lut(self, filename): 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 443c540b61a..e2168ce625f 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 fe76c86f40e..fe0d32155f9 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -42,7 +42,8 @@ def __init__(self, mode="RGB", palette=None, size=0): 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 @@ def tobytes(self): .. 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 @@ def getcolor(self, color, image=None): .. 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 @@ def getcolor(self, color, image=None): 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 @@ def getcolor(self, color, image=None): 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 @@ def save(self, fp): .. 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 a34678c7852..ad607a97b1a 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 9df910a4330..c4bb6334acf 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 9d52245889e..29d900befb4 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -124,7 +124,8 @@ def show_file(self, path=None, **options): 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 @@ def show_file(self, path=None, **options): 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 @@ def show_file(self, path=None, **options): 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 @@ def show_file(self, path=None, **options): 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 @@ def show_file(self, path=None, **options): 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 @@ def show_file(self, path=None, **options): 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 @@ def show_file(self, path=None, **options): 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 1baef7db499..b7ebddf066a 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -36,7 +36,8 @@ def __init__(self, image_or_list, mask=None): 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 949cf1fbf9d..09a6356fa47 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -284,7 +284,8 @@ def __init__(self, master, im): 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 dc707801274..cfeadd53c5c 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -41,7 +41,8 @@ def _open(self): 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 0bbe50668d8..77481756932 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -66,12 +66,14 @@ def field(self): # 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 @@ def _open(self): 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 11d1d488a29..7457874c1ec 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -44,13 +44,13 @@ def _can_read(self, num_bytes): 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 @@ def next_box_type(self): 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 @@ def _open(self): 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 eb0db5bb3da..9657ae9d0e8 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 @@ def _open(self): 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 @@ def _open(self): 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 @@ def load_djpeg(self): 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 validate_qtables(qtables): 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 validate_qtables(qtables): 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 cd047fe9d9d..8d4d826aa14 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -39,7 +39,8 @@ def _open(self): # 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 @@ def _open(self): 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 d4f6c90f778..e7e1054a3ad 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -47,7 +47,8 @@ def _open(self): 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 @@ def _open(self): # 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 @@ def seek(self, frame): 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 a358dfdce62..2d799d6d89e 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -67,7 +67,8 @@ def _open(self): 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 095cfe7ee96..b1ec2c7bc0a 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -143,7 +143,8 @@ def seek(self, frame): 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 c4d7ddbb4f8..5420894dceb 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -53,14 +53,16 @@ def _open(self): # 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 @@ def decode(self, buffer): 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 @@ def decode(self, buffer): 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 @@ def decode(self, buffer): 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 @@ def decode(self, buffer): 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 ee9dca86017..07acd55809f 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -34,7 +34,8 @@ def __init__(self, fp): 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 700f10e3f79..109aad9ab15 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 38caf5c63c1..5802d386a58 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -35,7 +35,8 @@ def _open(self): 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 442ac70c49d..ecce1b09745 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -63,7 +63,8 @@ def __init__(self, fp, charset_encoding="iso8859-1"): 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 @@ def _load_bitmaps(self, metrics): 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 841c18a2200..3202475dc12 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,12 +54,14 @@ def _open(self): # 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 @@ def _open(self): 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 404759a7fcb..baad4939f26 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 fd5cc5a61e3..e4a0f25a96d 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -138,9 +138,10 @@ def __delitem__(self, key): 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 @@ def decode(self): 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 @@ def get_literal_string(cls, data, offset): 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 c4860b6c4f3..8d0a34dbad3 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -44,7 +44,8 @@ def _open(self): # 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 b6a3c4cb660..b6626bbc549 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 @@ def read(self): 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 @@ def crc(self, cid, data): 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 @@ def verify(self, endchunk=b"IEND"): 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 @@ def __init__(self, fp): 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 @@ def chunk_iCCP(self, pos, length): 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 @@ def chunk_IHDR(self, pos, length): 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 @@ def chunk_IHDR(self, pos, length): 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 @@ def chunk_sRGB(self, pos, length): 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 @@ def chunk_pHYs(self, pos, length): 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 @@ def chunk_zTXt(self, pos, length): 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 @@ def chunk_acTL(self, pos, length): 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 @@ def chunk_fcTL(self, pos, length): 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 @@ def chunk_fdAT(self, pos, length): 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 @@ def verify(self): """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 @@ def seek(self, frame): 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 @@ def _seek(self, frame, rewind=False): 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 @@ def _seek(self, frame, rewind=False): 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 1670d9d64c5..dee2f1e155d 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -84,9 +84,11 @@ def _read_token(self): 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 @@ def _open(self): 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 @@ def _open(self): 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 @@ def _decode_bitonal(self): 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 @@ def _decode_blocks(self, maxval): 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 bd10e3b95dd..c1ca30a030b 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -65,7 +65,8 @@ def _open(self): 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 @@ def _open(self): 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 @@ def seek(self, layer): 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 read(size): # 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 039f5ceeacf..e9cb34ceda1 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -79,7 +79,8 @@ def __setitem__(self, xy, color): :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 @@ def __getitem__(self, xy): 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 f0207bb7756..d533c55e57e 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -60,7 +60,8 @@ def _open(self): 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 @@ def _open(self): 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 @@ def _open(self): 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 acafc320e64..1192c2d7326 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -110,14 +110,17 @@ def _open(self): 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 @@ def _open(self): 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 @@ def tell(self): 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 c03759a01e6..c64de4444df 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -54,7 +54,8 @@ def _open(self): # 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 @@ def _open(self): 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 @@ def _open(self): 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 d108362fc9f..20e8a083f96 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,14 @@ def __init__(self, tarfile, file): 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 cd454b755c0..53fe6ef5cb7 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -73,7 +73,8 @@ def _open(self): 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 @@ def _open(self): 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 @@ def _open(self): 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 fa3479b3597..431edfd9b66 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -500,14 +500,16 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): :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 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): @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 @@ def write_signed_rational(self, *values): 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 @@ def tobytes(self, offset=0): 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 @@ def _seek(self, frame): 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 @@ def _load_libtiff(self): 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 @@ def _load_libtiff(self): 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 @@ def _setup(self): """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 @@ def _setup(self): 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 @@ def _setup(self): 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 @@ def _setup(self): 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 @@ def _setup(self): 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 @@ def setup(self): 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 @@ def finalize(self): 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 @@ def rewriteLastShortToLong(self, value): 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 @@ def fixIFD(self): 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 @@ def fixOffsets(self, count, isShort=False, isLong=False): 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 81ed550d962..1d074f78c88 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -130,7 +130,8 @@ def _get_next(self): 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 2f54cdebbea..639730b8e4c 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -109,7 +109,8 @@ def _open(self): # 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 @@ def _open(self): 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 @@ def load(self, dpi=None): 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 4efedb77ea7..f0e05e8671b 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -49,7 +49,8 @@ def _open(self): # 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 @@ def _open(self): 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 59acabebae3..ad18e0031d5 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -53,7 +53,8 @@ def _open(self): 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 @@ def _open(self): 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 aaed2039db4..5fae4cd68dd 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 @@ def _open(self): 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 @@ def _open(self): ) 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 30a8a897100..7c4b1623d26 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 3838568f3a6..6f9d99e765a 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 a061aaf1743..68c2acd6701 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 @@ def build_pillow(): 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 169/727] 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 d0e9508fed8..875a2032699 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -201,9 +201,8 @@ def _open(self): 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 386fb7c26ea..b22060965a3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2129,7 +2129,7 @@ def resize(self, size, resample=None, box=None, reducing_gap=None): 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 @@ def resize(self, size, resample=None, box=None, reducing_gap=None): (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 @@ def __transformer( 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 @@ def __transformer( (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_core(fp, filename, prefix, formats): 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 2a2d372e52e..f87849680df 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 0d3facf5716..12391955fcf 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -63,12 +63,13 @@ 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 60cbbedc384..6fccc315b3d 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -146,7 +146,8 @@ def build_lut(self): 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 e4a0f25a96d..aa5ea2fbb06 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -817,10 +817,10 @@ def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): 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 @@ def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): 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 68c2acd6701..f5050946c78 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 @@ def build_pillow(): 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 170/727] 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 a37c90296eb..13c1628125a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -512,6 +512,11 @@ def test_check_size(self): 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 f7206022843..5b6bfb89cd8 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 171/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4eebbda6aa9..904c7362920 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 172/727] 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 a84a9fe3654..4d48b822a85 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 20a5afa6347..2b111d5e429 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 173/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 904c7362920..df24f562f0b 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 174/727] 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 219189cf208..f8b050f7625 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 9667b6a4aad..5299febe915 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 13c1628125a..890769fcd34 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -512,8 +512,11 @@ def test_check_size(self): 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 175/727] 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 40aabc3239f..616808a485d 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 25f03b34312..b400381d310 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 04823e2d7c7..fb58d25edb4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,7 @@ # 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 176/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index df24f562f0b..655089ab2eb 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 177/727] 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 00000000000..0465e48c204 --- /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 00000000000..020ddfcd973 --- /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 b52d6353ebc..3190988972d 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 178/727] 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 020ddfcd973..e8d612a7fc8 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -4,6 +4,7 @@ from .helper import skip_unless_feature + class TestFontCrash: def _fuzz_font(self, font): # from fuzzers.fuzz_font @@ -17,5 +18,5 @@ def _fuzz_font(self, font): @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 179/727] 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 629e9ac00d4..1b0b1d3dc03 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 e8d612a7fc8..9a2110c0c68 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -17,6 +17,6 @@ def _fuzz_font(self, font): @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 3190988972d..053ef1e7d5d 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 180/727] 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 1b0b1d3dc03..fb8f87e8658 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 9a2110c0c68..e8d612a7fc8 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -17,6 +17,6 @@ def _fuzz_font(self, font): @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 053ef1e7d5d..0db17a5a6db 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 181/727] 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 fb8f87e8658..dc111c38b36 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 e8d612a7fc8..27663f396ea 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 182/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 655089ab2eb..e6a49467438 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 183/727] 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 2b111d5e429..a0d26dc524c 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 184/727] 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 a0d26dc524c..2d83b7bf5c1 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 185/727] 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 2d83b7bf5c1..0af5bc8ca11 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 186/727] 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 6c6b07d610a..4e1c3a02121 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1271,7 +1271,8 @@ def draft(self, mode, size): 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 @@ def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): 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 @@ def transform( 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 187/727] 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 e6a49467438..7ec7b936da8 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 1cc1d0f1c7a..aca0aba021e 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 188/727] 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 aca0aba021e..7baa9fb6c55 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 189/727] 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 eabc6bf7593..fb8954125f6 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -447,7 +447,7 @@ def test_get_child_images(self): 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 190/727] 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 de3920cf5eb..861fb64f072 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 191/727] [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 d019d3e7fce..d790e7850f9 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 192/727] 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 e65b155b2dc..4b76e893f34 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -75,6 +75,10 @@ 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 193/727] 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 317db4c0120..fa88065f43c 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -64,7 +64,7 @@ def test_grabclipboard(self): ) 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 194/727] 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 b0506748470..27c21be8708 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 195/727] Added Fosstodon badge [ci skip] --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8ee68f9b848..9f549ece642 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 196/727] Added Fosstodon URL to setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index b562e293471..2dc552a2ce7 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 197/727] 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 9f549ece642..dd1d2c60f2b 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 27c21be8708..c203a9c1265 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 198/727] 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 dd1d2c60f2b..489d3db54d9 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 5bcd5afa5b4..674b31bd7f5 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 199/727] 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 506bb7008f7..6ad3aaba18f 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 200/727] 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 6ad3aaba18f..8225756ba38 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 201/727] 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 8225756ba38..ad503baec61 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 202/727] 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 0a7202a338d..44d6d14686a 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 3d8dbe6bbf5..66be02078ad 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 203/727] 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 01f75e9a35d..45c662bd669 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 7f6f666c33c..ad0abbbd92d 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 dda814c1f7d..0b3eeeb49d5 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 e2168ce625f..e7719fcf9ce 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 204/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7ec7b936da8..9a267bc9c2b 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 205/727] 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 ea9b33dfc91..43e244c7b40 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 206/727] 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 53ceb6df030..be49955dd7e 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -135,16 +135,15 @@ def test_reduce_hamming(self, mode): @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 207/727] 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 a78349801fc..7411f0b78d5 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -42,12 +42,12 @@ def test_palette(self): 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 @@ def test_quad(self): # 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 @@ def test_mesh(self): # 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 208/727] 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 7411f0b78d5..64a5c94596f 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -178,11 +178,13 @@ def _test_nearest(self, op, mode): 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 209/727] 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 e7719fcf9ce..16c83f4e4df 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 210/727] 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 aa5ea2fbb06..1b3cb52a2dc 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 f5050946c78..df39260e0a0 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 211/727] 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 9481cd5ddfe..133f3e47e14 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -593,7 +593,7 @@ def test_roundtrip_private_chunk(self): 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 d01315b2031..6ee1bd3d88a 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 4e1c3a02121..b0ff5173c51 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3841,7 +3841,7 @@ def hide_offsets(self): 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 431edfd9b66..431a95701da 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -257,7 +257,7 @@ (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 @@ def load_end(self): # 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 212/727] 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 3e54762227a..f0dedc2defc 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 c1ca30a030b..7e8d12759bd 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -238,15 +238,13 @@ def read(size): 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 213/727] 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 670b2f4ebde..6de2566b238 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 214/727] Convert test_optimize_correctness to use parametrize --- Tests/test_file_gif.py | 67 ++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index d48fc144205..6fbc0ee3009 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -158,39 +158,42 @@ def test_bilevel(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 - - assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - - # 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) + # Check for correctness after conversion back to 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 + + 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 215/727] 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 cfe46b65898..ece98f73d7a 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -24,6 +24,7 @@ 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 59e2c18b9ac..63d6dcf5cec 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 216/727] 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 42fe8c254fa..2a83ed151bf 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 c3134dc04994fa3125f127c6c5a54f03e294fa5e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 10 Jan 2023 00:03:07 -0600 Subject: [PATCH 217/727] refactor EpsImagePlugin Merge the PSFile class into the EpsImageFile class to hopefully improve performance. Also added a check for the required "%!PS-Adobe" and "%%BoundingBox" header comments. --- Tests/test_file_eps.py | 27 ++++++++- src/PIL/EpsImagePlugin.py | 113 +++++++++++++++++++++++--------------- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 015dda992c6..9558d149fd7 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -221,7 +221,7 @@ def test_read_binary_preview(): pass -def test_readline(tmp_path): +def test_readline_psfile(tmp_path): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' line_endings = ["\r\n", "\n", "\n\r", "\r"] @@ -256,6 +256,31 @@ def _test_readline_file_psfile(test_string, ending): _test_readline_file_psfile(s, ending) +@pytest.mark.parametrize( + "line_ending", + (b"\r\n", b"\n", b"\n\r", b"\r"), +) +def test_readline(line_ending): + simple_file = line_ending.join( + ( + b"%!PS-Adobe-3.0 EPSF-3.0", + b"%%Comment1: Some Value", + b"%%SecondComment: Another Value", + b"%%BoundingBox: 5 5 105 105", + b"10 setlinewidth", + b"10 10 moveto", + b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", + b"stroke", + ) + ) + + data = io.BytesIO(simple_file) + test_file = EpsImagePlugin.EpsImageFile(data) + assert test_file.info["Comment1"] == "Some Value" + assert test_file.info["SecondComment"] == "Another Value" + assert test_file.size == (100, 100) + + @pytest.mark.parametrize( "filename", ( diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 016e3c1353c..6b3e353f2a4 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -162,6 +162,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. + This class is no longer used internally, but kept for backwards-compatibility. """ def __init__(self, fp): @@ -194,7 +195,7 @@ def _accept(prefix): ## -# Image plugin for Encapsulated PostScript. This plugin supports only +# Image plugin for Encapsulated PostScript. This plugin supports only # a few variants of this format. @@ -209,29 +210,69 @@ class EpsImageFile(ImageFile.ImageFile): def _open(self): (length, offset) = self._find_offset(self.fp) - # Rewrap the open file pointer in something that will - # convert line endings and decode to latin-1. - fp = PSFile(self.fp) - # go to offset - start of "%!PS" - fp.seek(offset) - - box = None + self.fp.seek(offset) self.mode = "RGB" - self._size = 1, 1 # FIXME: huh? + self._size = None - # - # Load EPS header + byte_arr = bytearray(255) + bytes_mv = memoryview(byte_arr) + bytes_read = 0 + reading_comments = True - s_raw = fp.readline() - s = s_raw.strip("\r\n") + def check_required_header_comments(): + if "PS-Adobe" not in self.info: + msg = 'EPS header missing "%!PS-Adobe" comment' + raise SyntaxError(msg) + if "BoundingBox" not in self.info: + msg = 'EPS header missing "%%BoundingBox" comment' + raise SyntaxError(msg) - while s_raw: - if s: - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) + while True: + byte = self.fp.read(1) + if byte == b"": + # if we didn't read a byte we must be at the end of the file + if bytes_read == 0: + break + elif byte in b"\r\n": + # if we read a line ending character, ignore it and parse what + # we have already read. if we haven't read any other characters, + # continue reading + if bytes_read == 0: + continue + else: + # ASCII/hexadecimal lines in an EPS file must not exceed + # 255 characters, not including line ending characters + if bytes_read >= 255: + # only enforce this for lines starting with a "%", + # otherwise assume it's binary data + if byte_arr[0] == ord("%"): + msg = "not an EPS file" + raise SyntaxError(msg) + else: + if reading_comments: + check_required_header_comments() + reading_comments = False + # reset bytes_read so we can keep reading + # data until the end of the line + bytes_read = 0 + byte_arr[bytes_read] = byte[0] + bytes_read += 1 + continue + + if reading_comments: + # Load EPS header + + # if this line doesn't start with a "%", + # or does start with "%%EndComments", + # then we've reached the end of the header/comments + if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": + check_required_header_comments() + reading_comments = False + continue + + s = str(bytes_mv[:bytes_read], "latin-1") try: m = split.match(s) @@ -254,16 +295,12 @@ def _open(self): ] except Exception: pass - else: m = field.match(s) if m: k = m.group(1) - - if k == "EndComments": - break if k[:8] == "PS-Adobe": - self.info[k[:8]] = k[9:] + self.info["PS-Adobe"] = k[9:] else: self.info[k] = "" elif s[0] == "%": @@ -273,25 +310,11 @@ def _open(self): else: msg = "bad EPS header" raise OSError(msg) + elif bytes_mv[:11] == b"%ImageData:": + # Check for an "ImageData" descriptor - s_raw = fp.readline() - s = s_raw.strip("\r\n") - - if s and s[:1] != "%": - break - - # - # Scan for an "ImageData" descriptor - - while s[:1] == "%": - - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) - - if s[:11] == "%ImageData:": # Encoded bitmapped image. - x, y, bi, mo = s[11:].split(None, 7)[:4] + x, y, bi, mo = byte_arr[11:].split(None, 7)[:4] if int(bi) == 1: self.mode = "1" @@ -306,16 +329,16 @@ def _open(self): self._size = int(x), int(y) return - s = fp.readline().strip("\r\n") - if not s: - break + bytes_read = 0 - if not box: + check_required_header_comments() + + if not self._size: + self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) def _find_offset(self, fp): - s = fp.read(160) if s[:4] == b"%!PS": From 7f57c93b89804fd6468a264cac0350403a2a097b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Jan 2023 08:50:20 +1100 Subject: [PATCH 218/727] 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 016e3c1353c..f7d376364f3 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -316,21 +316,22 @@ def _open(self): 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 219/727] 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 ece98f73d7a..a2ef2280b72 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 2e45a33587c..5afe7cf5043 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 220/727] 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 674b31bd7f5..a4663bac8ef 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 221/727] 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 df39260e0a0..a34e8b34275 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ def cmd_msbuild( "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 222/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9a267bc9c2b..bf3017ca933 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 223/727] 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 | 552 ++++++++++++++++++++++++----- src/thirdparty/raqm/raqm.h | 15 + 5 files changed, 475 insertions(+), 102 deletions(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 99250365065..d1b31cfa53b 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 3354a4d2550..315e0c8d822 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 78b70a5615e..bdb6fb66264 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 13f6e1f023c..770ea30182b 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,13 +1077,159 @@ raqm_set_freetype_load_flags_range (raqm_t *rq, if (!rq->text_len) return true; - if (rq->text_utf8) + 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++) { - start = _raqm_u8_to_u32_index (rq, start); - end = _raqm_u8_to_u32_index (rq, end); + 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 _raqm_set_freetype_load_flags (rq, flags, start, end); + 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); } /** @@ -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 @@ _raqm_itemize (raqm_t *rq) } /* 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 @@ raqm_index_to_position (raqm_t *rq, * @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 bdb5a50d884..2fd836c8607 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 224/727] 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 2a83ed151bf..cc7d0258b10 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 0334e68f956a18350fc2cba872aef54e57c51e14 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 12 Jan 2023 08:36:17 -0600 Subject: [PATCH 225/727] add more eps file tests --- Tests/test_file_eps.py | 109 ++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 45 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 9558d149fd7..bded99cf13b 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -28,34 +28,47 @@ # EPS test files with binary preview FILE3 = "Tests/images/binary_preview_map.eps" +# Three unsigned 32bit little-endian values: +# 0xC6D3D0C5 magic number +# byte position of start of postscript section (12) +# byte length of postscript section (0) +# this byte length isn't valid, but we don't read it +simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00" + +# taken from page 8 of the specification +# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf +simple_eps_file = ( + b"%!PS-Adobe-3.0 EPSF-3.0", + b"%%BoundingBox: 5 5 105 105", + b"10 setlinewidth", + b"10 10 moveto", + b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", + b"stroke", +) +simple_eps_file_with_comments = ( + simple_eps_file[:1] + + ( + b"%%Comment1: Some Value", + b"%%SecondComment: Another Value", + ) + + simple_eps_file[1:] +) +simple_eps_file_without_version = simple_eps_file[1:] +simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_sanity(): - # Regular scale - with Image.open(FILE1) as image1: - image1.load() - assert image1.mode == "RGB" - assert image1.size == (460, 352) - assert image1.format == "EPS" - - with Image.open(FILE2) as image2: - image2.load() - assert image2.mode == "RGB" - assert image2.size == (360, 252) - assert image2.format == "EPS" - - # Double scale - with Image.open(FILE1) as image1_scale2: - image1_scale2.load(scale=2) - assert image1_scale2.mode == "RGB" - assert image1_scale2.size == (920, 704) - assert image1_scale2.format == "EPS" - with Image.open(FILE2) as image2_scale2: - image2_scale2.load(scale=2) - assert image2_scale2.mode == "RGB" - assert image2_scale2.size == (720, 504) - assert image2_scale2.format == "EPS" +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize( + ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) +) +@pytest.mark.parametrize("scale", (1, 2)) +def test_sanity(filename, size, scale): + expected_size = tuple(s * scale for s in size) + with Image.open(filename) as image: + image.load(scale=scale) + assert image.mode == "RGB" + assert image.size == expected_size + assert image.format == "EPS" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -69,18 +82,36 @@ def test_load(): def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(invalid_file) +def test_binary_header_only(): + data = io.BytesIO(simple_binary_header) + with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_version_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_boundingbox_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) + with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): + EpsImagePlugin.EpsImageFile(data) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" @@ -101,7 +132,7 @@ def test_showpage(): with Image.open("Tests/images/reqd_showpage.png") as target: # should not crash/hang plot_image.load() - # fonts could be slightly different + # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -112,7 +143,7 @@ def test_transparency(): assert plot_image.mode == "RGBA" with Image.open("Tests/images/reqd_showpage_transparency.png") as target: - # fonts could be slightly different + # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -207,7 +238,6 @@ def test_resize(filename): @pytest.mark.parametrize("filename", (FILE1, FILE2)) def test_thumbnail(filename): # Issue #619 - # Arrange with Image.open(filename) as im: new_size = (100, 100) im.thumbnail(new_size) @@ -256,24 +286,13 @@ def _test_readline_file_psfile(test_string, ending): _test_readline_file_psfile(s, ending) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize( "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(line_ending): - simple_file = line_ending.join( - ( - b"%!PS-Adobe-3.0 EPSF-3.0", - b"%%Comment1: Some Value", - b"%%SecondComment: Another Value", - b"%%BoundingBox: 5 5 105 105", - b"10 setlinewidth", - b"10 10 moveto", - b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", - b"stroke", - ) - ) - +def test_readline(prefix, line_ending): + simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) assert test_file.info["Comment1"] == "Some Value" From a2edefb45532d20cda0a2915d3f7363dd6ad8754 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Jan 2023 07:18:56 +1100 Subject: [PATCH 226/727] 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 ccf6e193a6d..6a60bc7f020 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 227/727] 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 05867b7d448..f8b985a7a02 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 a34e8b34275..fd12240e580 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -177,9 +177,9 @@ def cmd_msbuild( "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 228/727] 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 48797ea084d..1061f7d05f7 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 431a95701da..baa9abad8a2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -722,6 +722,8 @@ def load_byte(self, data, legacy_api=True): @_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 229/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bf3017ca933..b3dd16ace2c 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 230/727] 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 9657ae9d0e8..b9c80236e18 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -41,7 +41,7 @@ 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 231/727] 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 b0ff5173c51..7fc8f496eeb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3267,7 +3267,7 @@ def _open_core(fp, filename, prefix, formats): 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 232/727] 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 616808a485d..125bdcc44fb 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 489d3db54d9..af1ca57c25b 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 b400381d310..bc44ba388a6 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 fb58d25edb4..96324423a73 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,8 +52,8 @@ # 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 @@ master_doc, "PillowPILFork.tex", "Pillow (PIL Fork) Documentation", - "Alex Clark", + "Jeffrey A. Clark (Alex)", "manual", ) ] @@ -293,7 +293,7 @@ "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 a4663bac8ef..418844ba784 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 2dc552a2ce7..824cae08845 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 4b76e893f34..0e6f82092e4 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 233/727] [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 96324423a73..e1ffa49b8b2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,9 @@ # 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 234/727] 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 f7d376364f3..dd68c13e558 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -173,11 +173,13 @@ def seek(self, offset, whence=io.SEEK_SET): 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 235/727] 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 24336568199..b4ebbb9c26e 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 @@ def build_extensions(self): 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 4c2550db423523efedd773a3042754f2ad627477 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 15:29:23 -0600 Subject: [PATCH 236/727] add test for invalid bounding box --- Tests/test_file_eps.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index bded99cf13b..7abed6f42f7 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -55,6 +55,7 @@ ) simple_eps_file_without_version = simple_eps_file[1:] simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] +simple_eps_file_with_invalid_boundingbox = simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -106,6 +107,13 @@ def test_missing_boundingbox_comment(prefix): EpsImagePlugin.EpsImageFile(data) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) + with pytest.raises(OSError, match="cannot determine EPS bounding box"): + EpsImagePlugin.EpsImageFile(data) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) From 3d6770d0f33fdfc18a6833a384fbccb9ef878dfa Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 15:56:25 -0600 Subject: [PATCH 237/727] add tests for long lines --- Tests/test_file_eps.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 7abed6f42f7..26ac2e5a191 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -55,7 +55,21 @@ ) simple_eps_file_without_version = simple_eps_file[1:] simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] -simple_eps_file_with_invalid_boundingbox = simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] +simple_eps_file_with_invalid_boundingbox = ( + simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] +) +simple_eps_file_with_long_ascii_comment = ( + simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:] +) +simple_eps_file_with_long_binary_data = ( + simple_eps_file[:2] + + ( + b"%%BeginBinary: 300", + b"\0" * 300, + b"%%EndBinary", + ) + + simple_eps_file[2:] +) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -114,6 +128,30 @@ def test_invalid_boundingbox_comment(prefix): EpsImagePlugin.EpsImageFile(data) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_ascii_comment_too_long(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) + with pytest.raises(SyntaxError, match="not an EPS file"): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_long_binary_data(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_load_long_binary_data(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + with Image.open(data) as img: + img.load() + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) From cd4656410f8d8ddcf806717e7404bc2f0392d88d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 17:32:58 -0600 Subject: [PATCH 238/727] 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 5daab47fca3..49451ff44cb 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -10,18 +10,21 @@ 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 239/727] 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 49451ff44cb..799c243d655 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -11,7 +11,7 @@ @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 240/727] 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 6c4f1ceec04..83eb7a1b2de 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -139,15 +139,18 @@ def check(self, mode, c=None): # 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 @@ def check(self, mode, c=None): # check initial color im = Image.new(mode, (1, 1), c) - assert ( - im.getpixel((0, 0)) == c - ), f"initial color failed for mode {mode}, color {c} " + d = im.getpixel((0, 0)) + assert d == c, f"initial color failed for mode {mode}, expected {c} got {d}" + # check initial color negative index - assert ( - im.getpixel((-1, -1)) == c - ), f"initial color failed with negative index for mode {mode}, color {c} " + d = im.getpixel((-1, -1)) + assert d == c, ( + f"initial color failed with negative index for mode {mode}, " + f"expected {c} got {d}" + ) # Check 0 im = Image.new(mode, (0, 0), c) From e3dd4de1934c35144430de1a411d1df5bad84732 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 20:08:37 -0600 Subject: [PATCH 241/727] parametrize check_jpeg_leaks::test_qtables_leak() Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/check_jpeg_leaks.py | 71 +++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index ab8d7771992..940c0b00d5b 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -75,43 +75,42 @@ """ -def test_qtables_leak(): +standard_l_qtable = ( + # fmt: off + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99, + # fmt: on +) + +standard_chrominance_qtable = ( + # fmt: off + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + # fmt: on +) + + +@pytest.mark.parametrize( + "qtables", + ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ), +) +def test_qtables_leak(qtables): im = hopper("RGB") - - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None - ) - ] - - standard_chrominance_qtable = [ - int(s) - for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split( - None - ) - ] - - qtables = [standard_l_qtable, standard_chrominance_qtable] - for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) From 1603872f244da741feeac5a87d235e570723853f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 16 Jan 2023 07:46:11 -0600 Subject: [PATCH 242/727] 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 83eb7a1b2de..e22b8d74c32 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -132,24 +132,25 @@ def color(mode): 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 @@ def check(self, mode, c=None): 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 243/727] 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 e22b8d74c32..4079d935800 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -214,13 +214,13 @@ def test_basic(self, mode): 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 244/727] 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 64dd024bd7f..541ec8fdabb 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 cc7d0258b10..0cea725b4d7 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 245/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b3dd16ace2c..ed41d46c7e4 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 246/727] 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 7fc8f496eeb..833473f789d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3268,8 +3268,14 @@ def _open_core(fp, filename, prefix, formats): 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 fcf5b7ef8319730df1de5df76cdebe427ae4a4c3 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 11:33:15 +1100 Subject: [PATCH 247/727] Fixed merge conflicts during recent pull --- src/encode.c | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/encode.c b/src/encode.c index 21c42d915b8..33a2a37a7ca 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,10 +1214,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; + char * comment = NULL; + int add_plt = 0; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbn", + "ss|OOOsOnOOOssbbnzp", &mode, &format, &offset, @@ -1233,7 +1235,9 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &cinema_mode, &mct, &sgnd, - &fd)) { + &fd, + &comment, + &add_plt)) { return NULL; } @@ -1315,6 +1319,29 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } } + if (comment != NULL && strlen(comment) > 0) { + /* Size is stored as as an uint16, subtract 4 bytes for the header */ + if (strlen(comment) >= 65531) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 comment is too long"); + Py_DECREF(encoder); + return NULL; + } + + context->comment = strdup(comment); + + if (context->comment == NULL) { + PyErr_SetString( + PyExc_MemoryError, + "Couldn't allocate memory for JPEG 2000 comment"); + Py_DECREF(encoder); + return NULL; + } + } else { + context->comment = NULL; + } + if (quality_layers && PySequence_Check(quality_layers)) { context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; context->quality_layers = quality_layers; @@ -1332,6 +1359,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->cinema_mode = cine_mode; context->mct = mct; context->sgnd = sgnd; + context->add_plt = add_plt; return (PyObject *)encoder; } From de43bc99c873fe0c752f5e303c4a40f954b61912 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 11:37:14 +1100 Subject: [PATCH 248/727] Added support for jpeg2000 comments and PLT marker segments --- Tests/test_file_jpeg2k.py | 44 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 13 ++++++++ src/PIL/Jpeg2KImagePlugin.py | 4 +++ src/libImaging/Jpeg2K.h | 6 ++++ src/libImaging/Jpeg2KEncode.c | 17 +++++++++++ 5 files changed, 84 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0229b224304..f52c334020c 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,5 +1,6 @@ import os import re +import struct from io import BytesIO import pytest @@ -371,3 +372,46 @@ def test_crashes(test_file): im.load() except OSError: pass + + +def test_custom_comment(): + output_stream = BytesIO() + unique_comment = "This is a unique comment, which should be found below" + test_card.save(output_stream, "JPEG2000", comment=unique_comment) + output_stream.seek(0) + data = output_stream.read() + # Lazy method to determine if the comment is in the image generated + assert(bytes(unique_comment, "utf-8") in data) + + +def test_plt_marker(): + # Search the start of the codesteam for the PLT box (id 0xFF58) + opj_version = re.search(r"(\d+\.\d+)\.\d+$", features.version_codec("jpg_2000")) + assert opj_version is not None + + if float(opj_version[1]) >= 2.4: + out = BytesIO() + test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + out.seek(0) + while True: + box_bytes = out.read(2) + if len(box_bytes) == 0: + # End of steam encounterd and no PLT or SOD + break + jp2_boxid = struct.unpack(">H", box_bytes)[0] + + if jp2_boxid == 0xff4f: + # No length specifier for main header + continue + elif jp2_boxid == 0xff58: + # This is the PLT box we're looking for + return + elif jp2_boxid == 0xff93: + break + # SOD box encountered and no PLT, so it wasn't found + + jp2_boxlength = struct.unpack(">H", out.read(2))[0] + out.seek(jp2_boxlength - 2, os.SEEK_CUR) + + # The PLT box wasn't found + raise ValueError diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8f2..9128400ac3d 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -589,6 +589,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.1.0 +**comment** + Adds a custom comment to the file, replacing the default + "Created by OpenJPEG version" comment. + + .. versionadded:: 9.5.0 + +**add_plt** + If ``True`` then include a PLT (packet length, tile-part header) marker + segment in the produced file. + The default is to not include it. + + .. versionadded:: 9.5.0 + .. note:: To enable JPEG 2000 support, you need to build and install the OpenJPEG diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 7457874c1ec..754010c7c4f 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -328,6 +328,8 @@ def _save(im, fp, filename): mct = info.get("mct", 0) signed = info.get("signed", False) fd = -1 + comment = info.get("comment", None) + add_plt = info.get("add_plt", False) if hasattr(fp, "fileno"): try: @@ -350,6 +352,8 @@ def _save(im, fp, filename): mct, signed, fd, + comment, + add_plt ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index b28a0440acc..65728be5de4 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -97,6 +97,12 @@ typedef struct { /* PRIVATE CONTEXT (set by decoder) */ const char *error_msg; + /* Custom comment */ + char * comment; + + /* Include PLT marker segment */ + int add_plt; + } JPEG2KENCODESTATE; /* diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index db1c5c0c93a..bb280ae94cc 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -439,6 +439,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { params.tcp_mct = context->mct; } + if (context->comment) { + params.cp_comment = context->comment; + } + params.prog_order = context->progression; params.cp_cinema = context->cinema_mode; @@ -492,6 +496,14 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { opj_set_warning_handler(codec, j2k_warn, context); opj_setup_encoder(codec, ¶ms, image); + /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) + if (context->add_plt) { + const char * plt_option[2] = {"PLT=YES", NULL}; + opj_encoder_set_extra_options(codec, plt_option); + } +#endif + /* Start encoding */ if (!opj_start_compress(codec, image, stream)) { state->errcode = IMAGING_CODEC_BROKEN; @@ -624,7 +636,12 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { free((void *)context->error_msg); } + if (context->comment) { + free((void *)context->comment); + } + context->error_msg = NULL; + context->comment = NULL; return -1; } From 41b3ac8aed826679485913f4e1c94320408016e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Jan 2023 00:52:38 +0000 Subject: [PATCH 249/727] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg2k.py | 8 ++++---- src/PIL/Jpeg2KImagePlugin.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index f52c334020c..6107075d5e3 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -381,7 +381,7 @@ def test_custom_comment(): output_stream.seek(0) data = output_stream.read() # Lazy method to determine if the comment is in the image generated - assert(bytes(unique_comment, "utf-8") in data) + assert bytes(unique_comment, "utf-8") in data def test_plt_marker(): @@ -400,13 +400,13 @@ def test_plt_marker(): break jp2_boxid = struct.unpack(">H", box_bytes)[0] - if jp2_boxid == 0xff4f: + if jp2_boxid == 0xFF4F: # No length specifier for main header continue - elif jp2_boxid == 0xff58: + elif jp2_boxid == 0xFF58: # This is the PLT box we're looking for return - elif jp2_boxid == 0xff93: + elif jp2_boxid == 0xFF93: break # SOD box encountered and no PLT, so it wasn't found diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 754010c7c4f..001dcf39cda 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -353,7 +353,7 @@ def _save(im, fp, filename): signed, fd, comment, - add_plt + add_plt, ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) From d55563ca2519765473f87271f4f39ebd75a9150e Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 12:05:05 +1100 Subject: [PATCH 250/727] Update docs/handbook/image-file-formats.rst to fix lint Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/handbook/image-file-formats.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 9128400ac3d..f466ccac866 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -590,13 +590,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.1.0 **comment** - Adds a custom comment to the file, replacing the default + Adds a custom comment to the file, replacing the default "Created by OpenJPEG version" comment. .. versionadded:: 9.5.0 **add_plt** - If ``True`` then include a PLT (packet length, tile-part header) marker + If ``True`` then include a PLT (packet length, tile-part header) marker segment in the produced file. The default is to not include it. From e8307d74064d555524558a1dcb8828006ab3d65a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 18 Jan 2023 23:03:13 -0600 Subject: [PATCH 251/727] more imagepath tests --- Tests/test_imagepath.py | 83 +++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 861fb64f072..2b378d333b2 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -8,7 +8,6 @@ def test_path(): - p = ImagePath.Path(list(range(10))) # sequence interface @@ -39,48 +38,76 @@ def test_path(): p.transform((1, 0, 1, 0, 1, 1)) assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] - # alternative constructors - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0.0, 1.0]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([(0, 1)]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(0)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(1)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(array.array("f", [0, 1])) - assert list(p) == [(0.0, 1.0)] - arr = array.array("f", [0, 1]) - p = ImagePath.Path(arr.tobytes()) +@pytest.mark.parametrize( + "coords", + ( + (0, 1), + [0, 1], + (0.0, 1.0), + [0.0, 1.0], + ((0, 1),), + [(0, 1)], + ((0.0, 1.0),), + [(0.0, 1.0)], + array.array("f", [0, 1]), + array.array("f", [0.0, 1.0]), + ImagePath.Path((0, 1)), + ), +) +def test_path_constructors(coords): + # Arrange / Act + p = ImagePath.Path(coords) + + # Assert assert list(p) == [(0.0, 1.0)] -def test_invalid_coords(): +def test_path_constructor_text(): # Arrange - coords = ["a", "b"] + arr = array.array("f", (0, 1)) - # Act / Assert + # Act + p = ImagePath.Path(arr.tobytes()) + + # Assert + assert list(p) == [(0.0, 1.0)] + + +@pytest.mark.parametrize( + "coords", + ( + ("a", "b"), + ([0, 1],), + [[0, 1]], + ([0.0, 1.0],), + [[0.0, 1.0]], + ), +) +def test_invalid_path_constructors(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "incorrect coordinate type" -def test_path_odd_number_of_coordinates(): - # Arrange - coords = [0] - - # Act / Assert +@pytest.mark.parametrize( + "coords", + ( + (0,), + [0], + (0, 1, 2), + [0, 1, 2], + ), +) +def test_path_odd_number_of_coordinates(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "wrong number of coordinates" From b00bde977199968aa62c539b85ef3feb2338d080 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 22:52:41 +1100 Subject: [PATCH 252/727] Update Tests/test_file_jpeg2k.py fix spelling error Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_jpeg2k.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 6107075d5e3..ccc18772a47 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -396,7 +396,7 @@ def test_plt_marker(): while True: box_bytes = out.read(2) if len(box_bytes) == 0: - # End of steam encounterd and no PLT or SOD + # End of steam encountered and no PLT or SOD break jp2_boxid = struct.unpack(">H", box_bytes)[0] From 9b660db62de0cac22f2d1bf37aabbd412ee7bc62 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 20 Jan 2023 14:35:11 +0200 Subject: [PATCH 253/727] 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 30ed4a8081d..3375eb6b282 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -11,6 +11,11 @@ "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 7c4b1623d26..fa6e1d00c61 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 254/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ed41d46c7e4..27dbd69bb74 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 255/727] 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 541ec8fdabb..8b847b8943d 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 0cea725b4d7..ea8722c569d 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 256/727] 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 45c662bd669..0aa2f1119f6 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 4e8de9ac9a1e14593e17a2cd1ff82c45ed0900cd Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 25 Jan 2023 08:13:40 -0600 Subject: [PATCH 257/727] add path-from-bytes test Also `array.array("f", [0, 1]) == array.array("f", [0.0, 1.0])` so we didn't need both of them. --- Tests/test_imagepath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 2b378d333b2..7a517b6f61d 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -51,7 +51,7 @@ def test_path(): ((0.0, 1.0),), [(0.0, 1.0)], array.array("f", [0, 1]), - array.array("f", [0.0, 1.0]), + array.array("f", [0, 1]).tobytes(), ImagePath.Path((0, 1)), ), ) From 9eacaee399acc4b61d420bd7edee6a83e03a7e07 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 09:36:22 +1100 Subject: [PATCH 258/727] Document how to create universal2 wheels --- docs/installation.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index ea8722c569d..28a00d1b1a6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -68,6 +68,18 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow + While we provide binaries for both x86-64 and arm64, we do not provide universal2 + binaries. However, it is simple to combine our current binaries to create one:: + + python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow + python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow + python3 -m pip install delocate + + Then, with the names of the downloaded wheels, use Python to combine them:: + + from delocate.fuse import fuse_wheels + fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') + .. tab:: Windows We provide Pillow binaries for Windows compiled for the matrix of From e76fa1674e65a38092dcb4b7cbafb2eaaaaaa6c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 14:15:51 +1100 Subject: [PATCH 259/727] 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 1fc8161467b..34609314cdf 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -6,7 +6,7 @@ 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 @@ def __init__(self): 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 260/727] 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 0d1d03ac8c1..69246bfcf45 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -20,7 +20,7 @@ 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 24336568199..e7f4f476c75 100755 --- a/setup.py +++ b/setup.py @@ -567,9 +567,7 @@ def build_extensions(self): ): 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 b144c3dd279..81e9a640f18 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -1020,7 +1020,7 @@ def freetype(font): 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 261/727] 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 b4ebbb9c26e..4382c1a97ab 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 262/727] 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 d261638f941..ad3346b5a58 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -398,6 +398,17 @@ def test_alpha_inplace(self): 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 833473f789d..ad0d25add90 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 263/727] 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 1109cd15e99..6e111ebfc76 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1067,3 +1067,21 @@ def test_save_zero(self, compression, tmp_path): 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 4f3c8e39010..96db4cb5e71 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -84,24 +84,6 @@ def test_context_manager(self): 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 264/727] 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 6e111ebfc76..26feeb5407f 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -986,6 +986,36 @@ def test_open_missing_samplesperpixel(self): ) 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 96db4cb5e71..cd8a00ab0a0 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -100,36 +100,6 @@ def test_bigtiff(self): 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 265/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 27dbd69bb74..e35a55965e0 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 266/727] 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 d790e7850f9..5214d352d70 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 267/727] 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 7b8070d3477..1dfb36f448f 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 6a60bc7f020..24575f6c715 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 268/727] Replace SVN with Git for installing extra test images --- .appveyor.yml | 4 +++- .github/workflows/test-windows.yml | 5 ++++- depends/install_extra_test_images.sh | 23 ++++++++++------------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b817cd9d804..d4dd2dc953b 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 487c3586f02..48825dc3025 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 02da12d61a4..7381d3767dc 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 - - 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 +archive=test-images-main + +./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 269/727] 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 7b8070d3477..1dfb36f448f 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 6a60bc7f020..24575f6c715 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 270/727] 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 1dfb36f448f..45118143474 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 24575f6c715..ef8214649dc 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 271/727] 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 48825dc3025..cf160a9974c 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 7381d3767dc..ffdfe17f2e9 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 79e67cb5c33e5cef781e664439bc5c7371c1c04c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 31 Jan 2023 21:42:25 +1100 Subject: [PATCH 272/727] Removed default argument --- src/PIL/Jpeg2KImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 001dcf39cda..68a354e3f19 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -328,7 +328,7 @@ def _save(im, fp, filename): mct = info.get("mct", 0) signed = info.get("signed", False) fd = -1 - comment = info.get("comment", None) + comment = info.get("comment") add_plt = info.get("add_plt", False) if hasattr(fp, "fileno"): From 18ad4c867ff8a53a6c40d6fdf954799b976ab7f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 21:49:12 +1100 Subject: [PATCH 273/727] Use skip_unless_feature_version --- Tests/test_file_jpeg2k.py | 56 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index ccc18772a47..5669af7abe3 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -12,6 +12,7 @@ assert_image_similar, assert_image_similar_tofile, skip_unless_feature, + skip_unless_feature_version, ) EXTRA_DIR = "Tests/images/jpeg2000" @@ -384,34 +385,31 @@ def test_custom_comment(): assert bytes(unique_comment, "utf-8") in data +@skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) - opj_version = re.search(r"(\d+\.\d+)\.\d+$", features.version_codec("jpg_2000")) - assert opj_version is not None - - if float(opj_version[1]) >= 2.4: - out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) - out.seek(0) - while True: - box_bytes = out.read(2) - if len(box_bytes) == 0: - # End of steam encountered and no PLT or SOD - break - jp2_boxid = struct.unpack(">H", box_bytes)[0] - - if jp2_boxid == 0xFF4F: - # No length specifier for main header - continue - elif jp2_boxid == 0xFF58: - # This is the PLT box we're looking for - return - elif jp2_boxid == 0xFF93: - break - # SOD box encountered and no PLT, so it wasn't found - - jp2_boxlength = struct.unpack(">H", out.read(2))[0] - out.seek(jp2_boxlength - 2, os.SEEK_CUR) - - # The PLT box wasn't found - raise ValueError + out = BytesIO() + test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + out.seek(0) + while True: + box_bytes = out.read(2) + if len(box_bytes) == 0: + # End of steam encountered and no PLT or SOD + break + jp2_boxid = struct.unpack(">H", box_bytes)[0] + + if jp2_boxid == 0xFF4F: + # No length specifier for main header + continue + elif jp2_boxid == 0xFF58: + # This is the PLT box we're looking for + return + elif jp2_boxid == 0xFF93: + break + # SOD box encountered and no PLT, so it wasn't found + + jp2_boxlength = struct.unpack(">H", out.read(2))[0] + out.seek(jp2_boxlength - 2, os.SEEK_CUR) + + # The PLT box wasn't found + raise ValueError From ca97e2a3a51fcad8eab4d5466b944e512b82dadd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:31:52 +1100 Subject: [PATCH 274/727] Use _binary --- Tests/test_file_jpeg2k.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 5669af7abe3..56d8a7974bb 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,11 +1,17 @@ import os import re -import struct from io import BytesIO import pytest -from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features +from PIL import ( + Image, + ImageFile, + Jpeg2KImagePlugin, + UnidentifiedImageError, + _binary, + features, +) from .helper import ( assert_image_equal, @@ -393,11 +399,11 @@ def test_plt_marker(): out.seek(0) while True: box_bytes = out.read(2) - if len(box_bytes) == 0: + if not box_bytes: # End of steam encountered and no PLT or SOD break - jp2_boxid = struct.unpack(">H", box_bytes)[0] + jp2_boxid = _binary.i16be(box_bytes) if jp2_boxid == 0xFF4F: # No length specifier for main header continue @@ -405,10 +411,10 @@ def test_plt_marker(): # This is the PLT box we're looking for return elif jp2_boxid == 0xFF93: - break # SOD box encountered and no PLT, so it wasn't found + break - jp2_boxlength = struct.unpack(">H", out.read(2))[0] + jp2_boxlength = _binary.i16be(out.read(2)) out.seek(jp2_boxlength - 2, os.SEEK_CUR) # The PLT box wasn't found From 4bb50b1fa7fe5ef4323b9cb0c819bfba2b608f7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:24:47 +1100 Subject: [PATCH 275/727] Test comment too long --- Tests/test_file_jpeg2k.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 56d8a7974bb..a0fb75016d9 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -390,6 +390,10 @@ def test_custom_comment(): # Lazy method to determine if the comment is in the image generated assert bytes(unique_comment, "utf-8") in data + too_long_comment = " " * 65532 + with pytest.raises(ValueError): + test_card.save(output_stream, "JPEG2000", comment=too_long_comment) + @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): From 04e8a9b3e723a867f220124c26489ace5d2187e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:24:52 +1100 Subject: [PATCH 276/727] Removed unnecessary code --- src/encode.c | 6 ++---- src/libImaging/Jpeg2K.h | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/encode.c b/src/encode.c index 33a2a37a7ca..e8946dbaef1 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,7 +1214,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; - char * comment = NULL; + char *comment = NULL; int add_plt = 0; if (!PyArg_ParseTuple( @@ -1326,7 +1326,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { PyExc_ValueError, "JPEG 2000 comment is too long"); Py_DECREF(encoder); - return NULL; + return NULL; } context->comment = strdup(comment); @@ -1338,8 +1338,6 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { Py_DECREF(encoder); return NULL; } - } else { - context->comment = NULL; } if (quality_layers && PySequence_Check(quality_layers)) { diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index 65728be5de4..7bf8b4b0a74 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -98,7 +98,7 @@ typedef struct { const char *error_msg; /* Custom comment */ - char * comment; + char *comment; /* Include PLT marker segment */ int add_plt; From a119b19c074869267f186051af0d1b0878f9cca0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Feb 2023 08:36:06 +1100 Subject: [PATCH 277/727] 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 fd12240e580..89903c621b2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ def cmd_msbuild( 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 278/727] 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 db1c5c0c93a..62d22bcc6ff 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 279/727] 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 5299febe915..705505e8398 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ 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 baad4939f26..0b2938d6f34 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 280/727] 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 ffdfe17f2e9..941bfbe84c8 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 281/727] [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 5214d352d70..45c1f3c5f08 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 282/727] [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 08a55d349d5..c600c45ed1f 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -4,7 +4,6 @@ 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 d8d645189e6..f4a129f50c6 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 ed9aff9cc23..002a44a4f4d 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 5f6d523558a..9e79937e9f5 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 e330404d64e..76f185b9adc 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -10,7 +10,6 @@ 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 0f09c4b9915..ef378b24a91 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 015dda992c6..ac6e8444730 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 447888acd8d..d2f5a6d1760 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -12,7 +12,6 @@ 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 b8b999d70d0..70d4d76db94 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 6fbc0ee3009..bce72d1927d 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 roundtrip(im, *args, **kwargs): 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 fd427746e96..768ac12bd79 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -10,7 +10,6 @@ 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 20b4b9619af..98dc5443cc3 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -8,7 +8,6 @@ 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 55632909c6b..42275424d91 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 afb17b1afad..9c1c3cf170e 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 5cf93713be4..425e690d66b 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 2d0e6977a70..2d99528d37c 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,7 +11,6 @@ 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 eabc6bf7593..e3c5abcbd7e 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -57,7 +57,6 @@ def gen_random_image(self, size, mode="RGB"): 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 @@ def test_exif_rollback(self): 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 @@ def test_bad_mpo_header(self): # 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 @@ def test_save_tiff_with_dpi(self, tmp_path): # 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 @@ def test_dpi_tuple_from_exif(self): # 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 @@ def test_dpi_int_from_exif(self): # 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 @@ def test_dpi_from_dpcm_exif(self): # 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 @@ def test_dpi_exif_zero_division(self): # 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 @@ def test_dpi_exif_string(self): # 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 @@ def test_no_dpi_in_exif(self): # 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 @@ def test_invalid_exif(self): # 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 @@ def test_exif_x_resolution(self, tmp_path): 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 @@ def test_ifd_offset_exif(self): # 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 0229b224304..de622c47897 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 1109cd15e99..f886d3aae99 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -645,7 +645,6 @@ def test_save_bytesio(self): 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 @@ def check_write(libtiff): 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 50d7c590b7a..497052b05e9 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 5299febe915..216b93ca96a 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 im_generator(ims): 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 133f3e47e14..c4db9790524 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -78,7 +78,6 @@ def get_chunks(self, filename): 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 @@ def test_bad_ztxt(self): assert im.info == {"spam": "egg"} def test_bad_itxt(self): - im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -201,7 +199,6 @@ def test_bad_itxt(self): 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 @@ def test_trns_null(self): # 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 4f934375c7c..036cb9d4be5 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 0e3b705a295..011e208d8d8 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 05c78c3161b..edb3206038b 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 7d8b5139aa9..bac00e8554a 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 4f3c8e39010..70142747ca2 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -25,7 +25,6 @@ class TestFileTiff: def test_sanity(self, tmp_path): - filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -157,7 +156,6 @@ def test_set_legacy_api(self): 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 @@ def test_xyres_tiff(self): 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 @@ def test_xyres_fallback_tiff(self): 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 @@ def test_frame_order(self): def test___str__(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # Act ret = str(im.ifd) @@ -392,7 +387,6 @@ def test_dict(self): # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # v2 interface v2_tags = { 256: 55, @@ -630,7 +624,6 @@ def test_with_underscores(self, tmp_path): 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 1061f7d05f7..a4481d85f47 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 4f513d82bc8..037479f9fbb 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -18,10 +18,8 @@ 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 439cb15bca9..7c8b54fd173 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -6,7 +6,6 @@ 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 9c54c675560..d2c05b78a82 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 ae53d2b6357..9efe7ec1438 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -10,7 +10,6 @@ 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 ad3346b5a58..85e3ff55b4d 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -69,7 +69,6 @@ def test_exception_inheritance(self): 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 178cfcef359..a12ce329fd1 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -4,7 +4,6 @@ def test_sanity(): - with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index b839a7b140a..d0fea385433 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 4c4c41b7b51..d4723c92401 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 fc0fbfb9bbc..412bc10d9a8 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,7 +30,6 @@ 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 c9b2fd865b8..d390f3c1eec 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -21,7 +21,6 @@ def getmesh(self, im): def test_sanity(): - ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -419,7 +418,6 @@ def 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 5bda2811717..ac99ef38196 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -6,7 +6,6 @@ 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 861fb64f072..8f8a9f44915 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -8,7 +8,6 @@ def test_path(): - p = ImagePath.Path(list(range(10))) # sequence interface diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6af7e760204..62f52833227 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -6,7 +6,6 @@ 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 5717fe15036..b3b5db13ff2 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -6,7 +6,6 @@ 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 37ed3659d0a..f6818be46dc 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -4,7 +4,6 @@ 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 efcdab9ec43..dcdee3d416d 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 tobytes(mode): def test_convert(): - im = original.copy() verify(im.convert("I;16")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 3de7ec30f5c..a8bbcbdb8bc 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 23eb9e39f41..2f6d0588853 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 12f475df036..6e3fcec90e1 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -7,7 +7,6 @@ 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 34197c14f80..5bd9bacdb35 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -9,7 +9,6 @@ @skip_unless_feature("webp") class TestWebPLeaks(PillowLeakTestCase): - mem_limit = 3 * 1024 # kb iterations = 100 diff --git a/setup.py b/setup.py index 4382c1a97ab..8f7f223f8cd 100755 --- a/setup.py +++ b/setup.py @@ -430,7 +430,6 @@ def get_macos_sdk_path(self): return sdk_path def build_extensions(self): - library_dirs = [] include_dirs = [] @@ -917,7 +916,6 @@ def build_extensions(self): 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 e13b18f276e..5bda0a5b05d 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -223,7 +223,6 @@ def _bitmap(self, header=0, offset=0): # --------------- 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 @@ def decode(self, buffer): # 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 a0da1b78622..0425bbd750e 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 aedc6ce7f65..94efff34156 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 81c0314f020..cde9d42f09f 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 dd68c13e558..60cb46df916 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -286,7 +286,6 @@ def _open(self): # Scan for an "ImageData" descriptor while s[:1] == "%": - if len(s) > 255: msg = "not an EPS file" raise SyntaxError(msg) @@ -317,7 +316,6 @@ def _open(self): 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 536bc1fe695..1185ef2d341 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 86eb2d5a204..50948ec423a 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 66681939d9d..f4e89a03e02 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 c5fc80b37ba..5ec0a6632e3 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 8ddc6b40be1..d145d01f724 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 @@ def _open_subimage(self, index=1, subimage=0): self.tile = [] for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) y1 = min(ysize, y + ytile) @@ -174,7 +172,6 @@ def _open_subimage(self, index=1, subimage=0): ) elif compression == 1: - # FIXME: the fill decoder is not implemented self.tile.append( ( @@ -186,7 +183,6 @@ def _open_subimage(self, index=1, subimage=0): ) elif compression == 2: - internal_color_conversion = s[14] jpeg_tables = s[15] rawmode = self.rawmode @@ -234,7 +230,6 @@ def _open_subimage(self, index=1, subimage=0): 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 828a45ced77..994a6e8ebb2 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 3875dc86622..7dda4f14301 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 6ee1bd3d88a..eadee1560b3 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 @@ def _is_palette_needed(self, p): return False def _open(self): - # Screen s = self.fp.read(13) if not _accept(s): @@ -157,7 +155,6 @@ def seek(self, frame): raise EOFError(msg) from e def _seek(self, frame, update_image=True): - if frame == 0: # rewind self.__offset = 0 @@ -195,7 +192,6 @@ def _seek(self, frame, update_image=True): 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 b5c5e3ca400..8e801be0b8a 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -64,18 +64,15 @@ def sphere_decreasing(middle, pos): 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 @@ def __init__(self, fp): 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 2e9cbe58d20..d388928945a 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 @@ def __init__(self, fp): raise SyntaxError(msg) for i in range(256): - s = fp.readline() if not s: break @@ -55,5 +53,4 @@ def __init__(self, fp): 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 2088eb7b014..8a799f19caa 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 d6f2837393f..bba05ed65a7 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 e76d0c35a74..c2f050eddb3 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 @@ def __init__(self, fobj): 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 568e6d38db5..a188f8fdcea 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -185,7 +185,7 @@ def sizes(self): 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 875a2032699..746743f658c 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 @@ def _open(self): 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 @@ def _open(self): 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 @@ def _open(self): n += 1 else: - msg = "Syntax error in IM header: " + s.decode("ascii", "replace") raise SyntaxError(msg) @@ -252,7 +247,6 @@ def _open(self): self._fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": - # ifunc95 formats try: # use bit decoder (if necessary) @@ -332,7 +326,6 @@ def tell(self): 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 ad0d25add90..81123d0703b 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 ce29a163b1b..163828d31d2 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 12391955fcf..132490a8e23 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -395,7 +395,6 @@ def feed(self, data): # parse what we have if self.decoder: - if self.offset > 0: # skip header skip = min(len(self.data), self.offset) @@ -420,14 +419,12 @@ def feed(self, data): 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 b144c3dd279..bd13c391e1d 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 @@ def _load_pilfont(self, filename): 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 16c83f4e4df..301c593c790 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 fe0d32155f9..e455c04596c 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 29d900befb4..f0e73fb9075 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -390,7 +390,6 @@ def show_image(self, image, **options): 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 09a6356fa47..ef569ed2edd 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 cfeadd53c5c..ac267457b06 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -30,12 +30,10 @@ 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 @@ def _open(self): xsize = ysize = 0 while True: - if buffer: s = buffer[:1] buffer = buffer[1:] @@ -57,7 +54,6 @@ def _open(self): break if s == b"\x0C": - # image data begins self.tile = [ ( @@ -71,7 +67,6 @@ def _open(self): 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 77481756932..4c47b55c1a5 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 @@ def field(self): return tag, size def _open(self): - # load descriptive fields while True: offset = self.fp.tell() @@ -134,7 +132,6 @@ def _open(self): ] 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 b9c80236e18..d7ddbe0d9c4 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 @@ def _open(self): self.icclist = [] while True: - i = s[0] if i == 0xFF: s = s + self.fp.read(1) @@ -418,7 +415,6 @@ def load_read(self, read_bytes): return s def draft(self, mode, size): - if len(self.tile) != 1: return @@ -455,7 +451,6 @@ def draft(self, mode, size): 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 8d4d826aa14..17c008b9a6a 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 e7e1054a3ad..8dd9f2909cc 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 2d799d6d89e..d96d3a11c49 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -58,12 +58,10 @@ def read(self, bits): 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 b1ec2c7bc0a..f9261c77d68 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 5420894dceb..c6567b2ae62 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 @@ def decode(self, buffer): 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 07acd55809f..4a2c497fc49 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 @@ def __init__(self, fp): 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 109aad9ab15..a88a907917d 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -112,9 +112,7 @@ def build_prototype_image(): 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 5802d386a58..e390f3fe51d 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -24,12 +24,10 @@ 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 ecce1b09745..d5f510f0355 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 @@ def __init__(self, fp, charset_encoding="iso8859-1"): self.glyph[ch] = glyph def _getformat(self, tag): - format, size, offset = self.toc[tag] fp = self.fp @@ -108,7 +106,6 @@ def _getformat(self, tag): return fp, format, i16, i32 def _load_properties(self): - # # font properties @@ -136,7 +133,6 @@ def _load_properties(self): return properties def _load_metrics(self): - # # font metrics @@ -147,7 +143,6 @@ def _load_metrics(self): 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 @@ def _load_metrics(self): 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 @@ def _load_metrics(self): return metrics def _load_bitmaps(self, metrics): - # # bitmap data diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3202475dc12..f42c2456b4b 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 @@ def _open(self): 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 8d0a34dbad3..7eb82228a99 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 b6626bbc549..9078957dcbe 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 @@ def close(self): 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 @@ def crc_skip(self, cid, data): 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 @@ def rewind(self): 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 @@ def chunk_iCCP(self, pos, length): return s def chunk_IHDR(self, pos, length): - # image header s = ImageFile._safe_read(self.fp, length) if length < 13: @@ -446,7 +441,6 @@ def chunk_IHDR(self, pos, length): 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 @@ def chunk_IDAT(self, pos, length): 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 @@ def chunk_PLTE(self, pos, length): 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 @@ def chunk_sRGB(self, pos, length): 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 @@ def chunk_pHYs(self, pos, length): return s def chunk_tEXt(self, pos, length): - # text s = ImageFile._safe_read(self.fp, length) try: @@ -562,7 +551,6 @@ def chunk_tEXt(self, pos, length): return s def chunk_zTXt(self, pos, length): - # compressed text s = ImageFile._safe_read(self.fp, length) try: @@ -597,7 +585,6 @@ def chunk_zTXt(self, pos, length): 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 @@ def _open(self): 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 dee2f1e155d..5aa418044b1 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 7e8d12759bd..5a5d60d568c 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 read(size): raise SyntaxError(msg) for _ in range(abs(ct)): - # bounding box y0 = i32(read(4)) x0 = i32(read(4)) @@ -250,7 +247,6 @@ def read(size): def _maketile(file, mode, bbox, channels): - tile = None read = file.read diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index d533c55e57e..3662ffd1571 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -49,12 +49,10 @@ def _accept(prefix): ## # 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 1192c2d7326..eac27e679bd 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 @@ def tkPhotoImage(self): # -------------------------------------------------------------------- # 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 @@ def _save_spider(im, fp, filename): 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 c64de4444df..6712583d71c 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 20e8a083f96..32928f6af30 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -32,7 +32,6 @@ def __init__(self, tarfile, file): 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 53fe6ef5cb7..67dfc3d3c8e 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -46,12 +46,10 @@ class TgaImageFile(ImageFile.ImageFile): - format = "TGA" format_description = "Targa" def _open(self): - # process header s = self.fp.read(18) @@ -174,7 +172,6 @@ def load_end(self): 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 baa9abad8a2..2cf5b173f3a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -793,7 +793,6 @@ def _ensure_read(self, fp, size): return ret def load(self, fp): - self.reset() self._offset = fp.tell() @@ -938,7 +937,6 @@ def tobytes(self, offset=0): 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 @@ def __getitem__(self, tag): class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False @@ -1582,7 +1579,6 @@ def _setup(self): 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 0dc695a88d4..e4f47aa04bc 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -28,7 +28,6 @@ class WalImageFile(ImageFile.ImageFile): - format = "WAL" format_description = "Quake2 Texture" diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1d074f78c88..d060dd4b819 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 639730b8e4c..0ecab56a824 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 @@ def _open(self): 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 f0e05e8671b..aa4a01f4e5a 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 ad18e0031d5..3c12564c963 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 @@ def _open(self): 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 5fae4cd68dd..5d5bdc3edfa 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 @@ def _open(self): 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 @@ def _open(self): 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 @@ def _open(self): break else: - # missing colour key msg = "cannot read this XPM file" raise ValueError(msg) @@ -110,7 +104,6 @@ def _open(self): 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 be9aea35a892b5e551e17057252403ea5a4e0929 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 28 Jan 2023 14:22:05 -0600 Subject: [PATCH 283/727] add eps test for bad BoundingBox, good ImageData --- Tests/test_file_eps.py | 16 +++++++++++++++- src/PIL/EpsImagePlugin.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 26ac2e5a191..5d63df4a618 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -56,7 +56,10 @@ simple_eps_file_without_version = simple_eps_file[1:] simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] simple_eps_file_with_invalid_boundingbox = ( - simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] + simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:] +) +simple_eps_file_with_invalid_boundingbox_valid_imagedata = ( + simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",) ) simple_eps_file_with_long_ascii_comment = ( simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:] @@ -128,6 +131,17 @@ def test_invalid_boundingbox_comment(prefix): EpsImagePlugin.EpsImageFile(data) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): + data = io.BytesIO( + prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) + ) + with Image.open(data) as img: + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) def test_ascii_comment_too_long(prefix): data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 4c0ab0e127b..2a4e804ce21 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -314,7 +314,7 @@ def check_required_header_comments(): # Check for an "ImageData" descriptor # Encoded bitmapped image. - x, y, bi, mo = byte_arr[11:].split(None, 7)[:4] + x, y, bi, mo = byte_arr[11:bytes_read].split(None, 7)[:4] if int(bi) == 1: self.mode = "1" From bd0fac80c4f439d6fdb706889beb9b8c627c1ee8 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 6 Feb 2023 17:23:57 -0600 Subject: [PATCH 284/727] deprecate EpsImagePlugin.PSFile --- Tests/test_file_eps.py | 6 ++++++ docs/deprecations.rst | 10 ++++++++++ docs/releasenotes/9.4.0.rst | 11 +++++++++++ src/PIL/EpsImagePlugin.py | 6 ++++++ src/PIL/_deprecate.py | 4 +++- 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 5d63df4a618..e4c1000e26a 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -311,6 +311,7 @@ def test_read_binary_preview(): pass +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_readline_psfile(tmp_path): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' @@ -346,6 +347,11 @@ def _test_readline_file_psfile(test_string, ending): _test_readline_file_psfile(s, ending) +def test_psfile_deprecation(): + with pytest.warns(DeprecationWarning): + EpsImagePlugin.PSFile(None) + + @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize( "line_ending", diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a85..c31b0dac949 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -80,6 +80,16 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 was reversed in Pillow 9.4.0 and those constants will now remain available. See :ref:`restored-image-constants` +PSFile +~~~~~~ + +.. deprecated:: 9.4.0 + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 0af5bc8ca11..b7a63dd61e7 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,6 +1,17 @@ 9.4.0 ----- +Deprecations +============ + +PSFile +^^^^^^ + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + API Additions ============= diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 2a4e804ce21..6c63ef08a38 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -29,6 +29,7 @@ from . import Image, ImageFile from ._binary import i32le as i32 +from ._deprecate import deprecate # # -------------------------------------------------------------------- @@ -166,6 +167,11 @@ class PSFile: """ def __init__(self, fp): + deprecate( + "PSFile", + 11, + action="If you need the functionality of this class you will need to implement it yourself.", + ) self.fp = fp self.char = None diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 7c4b1623d26..81f2189dcfc 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -47,8 +47,10 @@ def deprecate( raise RuntimeError(msg) elif when == 10: removed = "Pillow 10 (2023-07-01)" + elif when == 11: + removed = "Pillow 11 (2024-10-15)" else: - msg = f"Unknown removal version, update {__name__}?" + msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) if replacement and action: From 62ab8bf80c79e1c7d8c16cf32174421f64cc8c0c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 6 Feb 2023 18:00:31 -0600 Subject: [PATCH 285/727] update "unknown version" deprecation test --- Tests/test_deprecate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 3375eb6b282..c7a7a9ff50f 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -29,7 +29,7 @@ def test_version(version, expected): def test_unknown_version(): - expected = r"Unknown removal version, update PIL\._deprecate\?" + expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" with pytest.raises(ValueError, match=expected): _deprecate.deprecate("Old thing", 12345, "new thing") From 99b153c9cadbcb1c39c6747c3d97057aef054517 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:49:00 -0600 Subject: [PATCH 286/727] hyphenate "backwards-compatibility" Co-authored-by: Hugo van Kemenade --- src/PIL/EpsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 6c63ef08a38..48d32498f2b 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -163,7 +163,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. - This class is no longer used internally, but kept for backwards-compatibility. + This class is no longer used internally, but kept for backwards compatibility. """ def __init__(self, fp): From 0f27ddafb710e104d84605a26a0e58f2a6495019 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:56:38 -0600 Subject: [PATCH 287/727] split long line --- src/PIL/EpsImagePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 48d32498f2b..9da6e946bee 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -170,7 +170,8 @@ def __init__(self, fp): deprecate( "PSFile", 11, - action="If you need the functionality of this class you will need to implement it yourself.", + action="If you need the functionality of this class " + "you will need to implement it yourself.", ) self.fp = fp self.char = None From dd985b2a5e265118c36683b20afa4153a75cf69f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:58:05 -0600 Subject: [PATCH 288/727] make deprecation check more specific --- Tests/test_file_eps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index e4c1000e26a..26adfff8786 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -311,7 +311,6 @@ def test_read_binary_preview(): pass -@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_readline_psfile(tmp_path): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' @@ -329,7 +328,8 @@ def _test_readline(t, ending): def _test_readline_io_psfile(test_string, ending): f = io.BytesIO(test_string.encode("latin-1")) - t = EpsImagePlugin.PSFile(f) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) def _test_readline_file_psfile(test_string, ending): @@ -338,7 +338,8 @@ def _test_readline_file_psfile(test_string, ending): w.write(test_string.encode("latin-1")) with open(f, "rb") as r: - t = EpsImagePlugin.PSFile(r) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(r) _test_readline(t, ending) for ending in line_endings: From e79460e775a69c2cd9896144c3b340c4f98e3ad5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 09:46:01 +1100 Subject: [PATCH 289/727] 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 ef8214649dc..737da7b94c7 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 290/727] Updated test images origin --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 790404535f7..1dd6c917524 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 291/727] 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 cf160a9974c..e938e999d1b 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 292/727] 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 81123d0703b..d47c5733468 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1432,6 +1432,11 @@ def get_value(element): 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 @@ def _apply_env_variables(env=None): 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 293/727] 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 941bfbe84c8..1ef6f4e9783 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 294/727] 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 d9608e7827a..a318bfafd9f 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 295/727] 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 89903c621b2..8a449410340 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ def cmd_msbuild( 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 296/727] 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 d47c5733468..f5fa4ec0db7 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 297/727] [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 f5fa4ec0db7..813c237ad4d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1434,7 +1434,7 @@ def get_value(element): 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 @@ def _apply_env_variables(env=None): 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 298/727] 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 8a449410340..ff95581fa7e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -253,9 +253,9 @@ def cmd_msbuild( "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 299/727] 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 8f7f223f8cd..0c8b390dec5 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 300/727] Update HPND wording in LICENSE file --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 125bdcc44fb..cf65e86d734 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 301/727] 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 813c237ad4d..a280935a54b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1433,7 +1433,7 @@ def get_value(element): 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 @@ def _apply_env_variables(env=None): 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 + + 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:: - 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`. + 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 302/727] 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 e3c5abcbd7e..b8466133091 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -270,7 +270,10 @@ def test_large_exif(self, tmp_path): # 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 d7ddbe0d9c4..71ae84c044a 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -730,10 +730,10 @@ def validate_qtables(qtables): 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 validate_qtables(qtables): 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 303/727] 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 9b527713864..ac048ba562f 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -312,7 +312,7 @@ def lookup(tag, group=None): 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 304/727] 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 518b66acc23..6aa122cc56e 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 dfd7d055302..1fc6262f4fe 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 11c7b77be03..8e06de4cc27 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 ea8722c569d..93260e1414f 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 305/727] 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 05e1370f6e3..cece2e93a33 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 0ce10e2493c..a50fc3e242b 100644 --- a/src/display.c +++ b/src/display.c @@ -421,79 +421,6 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { 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 306/727] 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 e938e999d1b..306e34ca91b 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 93260e1414f..1bfc65f4b96 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 307/727] 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 7331cf8ee0d..7d2b20d65cc 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 308/727] 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 11c7b77be03..87cad1f36cf 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 309/727] 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 1bfc65f4b96..1b5719a8e34 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 310/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e35a55965e0..626860e7158 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 311/727] 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 a280935a54b..63bad83a1c3 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 312/727] 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 ff95581fa7e..bef8afa9d8e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ def cmd_msbuild( "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 407489a0dc68c5e0f6143b3c767fe6f93245d81e Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 12 Feb 2023 23:24:11 +0000 Subject: [PATCH 313/727] windows: use CMake instead of MSBuild to compile liblzma --- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- winbuild/build_prepare.py | 22 +++++++--------------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index d8538fbf392..67aac597bc3 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -10,7 +10,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. -* Requires CMake 3.12 or newer (available as Visual Studio component). +* Requires CMake 3.13 or newer (available as Visual Studio component). * Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). diff --git a/winbuild/build.rst b/winbuild/build.rst index 71666977127..45a42a8ae77 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -21,7 +21,7 @@ Download and install: `_ (MSVC C++ build tools, and any Windows SDK version required) -* `CMake 3.12 or newer `_ +* `CMake 3.13 or newer `_ (also available as Visual Studio component C++ CMake tools for Windows) * x86/x64: `NASM `_ diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bef8afa9d8e..df207fd7efc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -156,25 +156,15 @@ def cmd_msbuild( "filename": "xz-5.4.1.tar.gz", "dir": "xz-5.4.1", "license": "COPYING", - "patch": { - r"src\liblzma\api\lzma.h": { - "#ifndef LZMA_API_IMPORT": "#ifndef LZMA_API_IMPORT\n#define LZMA_API_STATIC", # noqa: E501 - }, - r"windows\vs2019\liblzma.vcxproj": { - # retarget to default toolset (selected by vcvarsall.bat) - "v142": "$(DefaultPlatformToolset)", # noqa: E501 - # retarget to latest (selected by vcvarsall.bat) - "10.0": "$(WindowsSDKVersion)", # noqa: E501 - }, - }, "build": [ - cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Clean"), - cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Build"), + cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), + cmd_nmake(target="clean"), + cmd_nmake(target="liblzma"), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], "headers": [r"src\liblzma\api\lzma.h"], - "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], + "libs": [r"liblzma.lib"], }, "libwebp": { "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz", @@ -215,7 +205,9 @@ def cmd_msbuild( }, }, "build": [ - cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), + cmd_cmake( + "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DCMAKE_C_FLAGS=-DLZMA_API_STATIC" + ), cmd_nmake(target="clean"), cmd_nmake(target="tiff"), ], From abd2a3f7ee5a08ebbff3c21cd4658669d37a2fa8 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 12 Apr 2020 13:50:31 +0200 Subject: [PATCH 314/727] windows: compile dependencies with ninja instead of nmake --- winbuild/build.rst | 6 ++- winbuild/build_prepare.py | 100 +++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index 45a42a8ae77..a8d53680b17 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -24,7 +24,10 @@ Download and install: * `CMake 3.13 or newer `_ (also available as Visual Studio component C++ CMake tools for Windows) -* x86/x64: `NASM `_ +* `Ninja `_ + (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) + +* x86/x64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -53,6 +56,7 @@ behaviour of ``build_prepare.py``: ``build_prepare.py`` also supports the following command line parameters: * ``-v`` will print generated scripts. +* ``--nmake`` will use NMake instead of Ninja for CMake dependencies * ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency * ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi (required for Raqm text shaping). diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df207fd7efc..94ecd09bf29 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -55,24 +55,28 @@ def cmd_nmake(makefile=None, target="", params=None): ) -def cmd_cmake(params=None, file="."): - if params is None: - params = "" - elif isinstance(params, (list, tuple)): - params = " ".join(params) +def cmds_cmake(target, *params): + if isinstance(target, str): + targets = ("clean", target) else: - params = str(params) - return " ".join( - [ - "{cmake}", - "-DCMAKE_VERBOSE_MAKEFILE=ON", - "-DCMAKE_RULE_MESSAGES:BOOL=OFF", - "-DCMAKE_BUILD_TYPE=Release", - f"{params}", - '-G "NMake Makefiles"', - f'"{file}"', - ] - ) + targets = ("clean", *target) + + return [ + " ".join( + [ + "{cmake}", + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake + "-DCMAKE_C_COMPILER=cl.exe", # for Ninja + "-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja + *params, + '-G "{cmake_generator}"', + ".", + ] + ), + *(f"{{cmake}} --build . --target {tgt}" for tgt in targets), + ] def cmd_msbuild( @@ -118,19 +122,14 @@ def cmd_msbuild( ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" ), "build": [ - cmd_cmake( - [ - "-DENABLE_SHARED:BOOL=FALSE", - "-DWITH_JPEG8:BOOL=TRUE", - "-DWITH_CRT_DLL:BOOL=TRUE", - ] + *cmds_cmake( + ("jpeg-static", "cjpeg-static", "djpeg-static"), + "-DENABLE_SHARED:BOOL=FALSE", + "-DWITH_JPEG8:BOOL=TRUE", + "-DWITH_CRT_DLL:BOOL=TRUE", ), - cmd_nmake(target="clean"), - cmd_nmake(target="jpeg-static"), cmd_copy("jpeg-static.lib", "libjpeg.lib"), - cmd_nmake(target="cjpeg-static"), cmd_copy("cjpeg-static.exe", "cjpeg.exe"), - cmd_nmake(target="djpeg-static"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], "headers": ["j*.h"], @@ -157,9 +156,7 @@ def cmd_msbuild( "dir": "xz-5.4.1", "license": "COPYING", "build": [ - cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), - cmd_nmake(target="clean"), - cmd_nmake(target="liblzma"), + *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], @@ -205,11 +202,11 @@ def cmd_msbuild( }, }, "build": [ - cmd_cmake( - "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DCMAKE_C_FLAGS=-DLZMA_API_STATIC" - ), - cmd_nmake(target="clean"), - cmd_nmake(target="tiff"), + *cmds_cmake( + "tiff", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DCMAKE_C_FLAGS=-DLZMA_API_STATIC", + ) ], "headers": [r"libtiff\tiff*.h"], "libs": [r"libtiff\*.lib"], @@ -221,10 +218,7 @@ def cmd_msbuild( "dir": "lpng1639", "license": "LICENSE", "build": [ - # lint: do not inline - cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), - cmd_nmake(target="clean"), - cmd_nmake(), + *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), cmd_copy("libpng16_static.lib", "libpng16.lib"), ], "headers": [r"png*.h"], @@ -236,10 +230,7 @@ def cmd_msbuild( "dir": "brotli-1.0.9", "license": "LICENSE", "build": [ - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="brotlicommon-static"), - cmd_nmake(target="brotlidec-static"), + *cmds_cmake(("brotlicommon-static", "brotlidec-static")), cmd_xcopy(r"c\include", "{inc_dir}"), ], "libs": ["*.lib"], @@ -317,9 +308,9 @@ def cmd_msbuild( } }, "build": [ - cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), - cmd_nmake(target="clean"), - cmd_nmake(target="openjp2"), + *cmds_cmake( + "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" + ), cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), ], @@ -338,10 +329,7 @@ def cmd_msbuild( } }, "build": [ - # lint: do not inline - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="imagequant_a"), + *cmds_cmake("imagequant_a"), cmd_copy("imagequant_a.lib", "imagequant.lib"), ], "headers": [r"*.h"], @@ -354,9 +342,7 @@ def cmd_msbuild( "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), - cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), - cmd_nmake(target="clean"), - cmd_nmake(target="harfbuzz"), + *cmds_cmake("harfbuzz", "-DHB_HAVE_FREETYPE:BOOL=TRUE"), ], "headers": [r"src\*.h"], "libs": [r"*.lib"], @@ -369,9 +355,7 @@ def cmd_msbuild( "build": [ cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="fribidi"), + *cmds_cmake("fribidi"), ], "bins": [r"*.dll"], }, @@ -600,10 +584,13 @@ def build_pillow(): else ("x86" if struct.calcsize("P") == 4 else "x64"), ) build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) + cmake_generator = "Ninja" sources_dir = "" for arg in sys.argv[1:]: if arg == "-v": verbose = True + elif arg == "--nmake": + cmake_generator = "NMake Makefiles" elif arg == "--no-imagequant": disabled += ["libimagequant"] elif arg == "--no-raqm" or arg == "--no-fribidi": @@ -679,6 +666,7 @@ def build_pillow(): # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically + "cmake_generator": cmake_generator, # TODO find NASM automatically # script header "header": sum([header, msvs["header"], ["@echo on"]], []), From 57260d49242dded68dfd0287a2c3d17eb327a81c Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 13 Feb 2023 01:34:00 +0000 Subject: [PATCH 315/727] suppress MSVC compiler logo output when using ninja --- winbuild/build_prepare.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 94ecd09bf29..f643c4a0810 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -56,10 +56,8 @@ def cmd_nmake(makefile=None, target="", params=None): def cmds_cmake(target, *params): - if isinstance(target, str): - targets = ("clean", target) - else: - targets = ("clean", *target) + if not isinstance(target, str): + target = " ".join(target) return [ " ".join( @@ -70,12 +68,14 @@ def cmds_cmake(target, *params): "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake "-DCMAKE_C_COMPILER=cl.exe", # for Ninja "-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja + "-DCMAKE_C_FLAGS=-nologo", + "-DCMAKE_CXX_FLAGS=-nologo", *params, '-G "{cmake_generator}"', ".", ] ), - *(f"{{cmake}} --build . --target {tgt}" for tgt in targets), + f"{{cmake}} --build . --clean-first --parallel --target {target}", ] @@ -205,7 +205,7 @@ def cmd_msbuild( *cmds_cmake( "tiff", "-DBUILD_SHARED_LIBS:BOOL=OFF", - "-DCMAKE_C_FLAGS=-DLZMA_API_STATIC", + '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', ) ], "headers": [r"libtiff\tiff*.h"], @@ -341,8 +341,11 @@ def cmd_msbuild( "dir": "harfbuzz-7.0.0", "license": "COPYING", "build": [ - cmd_set("CXXFLAGS", "-d2FH4-"), - *cmds_cmake("harfbuzz", "-DHB_HAVE_FREETYPE:BOOL=TRUE"), + *cmds_cmake( + "harfbuzz", + "-DHB_HAVE_FREETYPE:BOOL=TRUE", + '-DCMAKE_CXX_FLAGS="-nologo -d2FH4-"', + ), ], "headers": [r"src\*.h"], "libs": [r"*.lib"], From eeb7c7c647cacc59189a5ee7891d38f23d19721a Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 13 Feb 2023 03:16:04 +0000 Subject: [PATCH 316/727] windows: parse build configuration with argparse --- .github/workflows/test-windows.yml | 2 +- winbuild/build.rst | 66 ++++++----- winbuild/build_prepare.py | 181 ++++++++++++++++++----------- 3 files changed, 153 insertions(+), 96 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 306e34ca91b..30084d093de 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -88,7 +88,7 @@ jobs: - name: Prepare build if: steps.build-cache.outputs.cache-hit != 'true' run: | - & python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir + & python.exe winbuild\build_prepare.py -v --python $env:pythonLocation shell: pwsh - name: Build dependencies / libjpeg-turbo diff --git a/winbuild/build.rst b/winbuild/build.rst index a8d53680b17..c13acf50d14 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -38,42 +38,50 @@ Visual Studio is found automatically with ``vswhere.exe``. Build configuration ------------------- -The following environment variables, if set, will override the default -behaviour of ``build_prepare.py``: - -* ``PYTHON`` + ``EXECUTABLE`` point to the target version of Python. - If ``PYTHON`` is unset, the version of Python used to run - ``build_prepare.py`` will be used. If only ``PYTHON`` is set, - ``EXECUTABLE`` defaults to ``python.exe``. -* ``ARCHITECTURE`` is used to select a ``x86``, ``x64`` or ``ARM64`` build. - By default, uses same architecture as the version of Python used to run ``build_prepare.py``. -* ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory - path, used to store generated build scripts and compiled libraries. - **Warning:** This directory is wiped when ``build_prepare.py`` is run. -* ``PILLOW_DEPS`` points to the directory used to store downloaded - dependencies. By default ``winbuild\depends`` is used. - -``build_prepare.py`` also supports the following command line parameters: - -* ``-v`` will print generated scripts. -* ``--nmake`` will use NMake instead of Ninja for CMake dependencies -* ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency -* ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi - (required for Raqm text shaping). -* ``--python=`` and ``--executable=`` override ``PYTHON`` and ``EXECUTABLE``. -* ``--architecture=`` overrides ``ARCHITECTURE``. -* ``--dir=`` and ``--depends=`` override ``PILLOW_BUILD`` - and ``PILLOW_DEPS``. +Run ``build_prepare.py`` to configure the build:: + + usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] + [--depends PILLOW_DEPS] + [--architecture {x86,x64,ARM64}] + [--python PYTHON] [--executable EXECUTABLE] + [--nmake] [--no-imagequant] [--no-fribidi] + + Download dependencies and generate build scripts for Pillow. + + options: + -h, --help show this help message and exit + -v, --verbose print generated scripts + -d PILLOW_BUILD, --dir PILLOW_BUILD, --build-dir PILLOW_BUILD + build directory (default: 'winbuild\build') + --depends PILLOW_DEPS + directory used to store cached dependencies (default: + 'winbuild\depends') + --architecture {x86,x64,ARM64} + build architecture (default: same as host python) + --python PYTHON Python install directory (default: use host python) + --executable EXECUTABLE + Python executable (default: use host python) + --nmake build dependencies using NMake instead of Ninja + --no-imagequant skip GPL-licensed optional dependency libimagequant + --no-fribidi, --no-raqm + skip LGPL-licensed optional dependency FriBiDi + + Arguments can also be supplied using the environment variables PILLOW_BUILD, + PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. See winbuild\build.rst for more + information. + +**Warning:** The build directory is wiped when ``build_prepare.py`` is run. Dependencies ------------ Dependencies will be automatically downloaded by ``build_prepare.py``. By default, downloaded dependencies are stored in ``winbuild\depends``; -set the ``PILLOW_DEPS`` environment variable to override this location. +use the ``--depends`` argument or ``PILLOW_DEPS`` environment variable +to override this location. To build all dependencies, run ``winbuild\build\build_dep_all.cmd``, -or run the individual scripts to build each dependency separately. +or run the individual scripts in order to build each dependency separately. Building Pillow --------------- @@ -106,7 +114,7 @@ The following is a simplified version of the script used on AppVeyor: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild - C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends + C:\Python37\bin\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd build\build_pillow.cmd install cd .. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f643c4a0810..de07810a84c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,3 +1,4 @@ +import argparse import os import platform import re @@ -434,7 +435,7 @@ def extract_dep(url, filename): import urllib.request import zipfile - file = os.path.join(depends_dir, filename) + file = os.path.join(args.depends_dir, filename) if not os.path.exists(file): ex = None for i in range(3): @@ -475,12 +476,12 @@ def extract_dep(url, filename): def write_script(name, lines): - name = os.path.join(build_dir, name) + name = os.path.join(args.build_dir, name) lines = [line.format(**prefs) for line in lines] print("Writing " + name) with open(name, "w", newline="") as f: f.write(os.linesep.join(lines)) - if verbose: + if args.verbose: for line in lines: print(" " + line) @@ -549,11 +550,14 @@ def build_dep(name): def build_dep_all(): lines = ["@echo on"] for dep_name in deps: + print() if dep_name in disabled: + print(f"Skipping disabled dependency {dep_name}") continue script = build_dep(dep_name) lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + print() lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines) @@ -572,59 +576,90 @@ def build_pillow(): if __name__ == "__main__": - # winbuild directory winbuild_dir = os.path.dirname(os.path.realpath(__file__)) - - verbose = False - disabled = [] - depends_dir = os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")) - python_dir = os.environ.get("PYTHON") - python_exe = os.environ.get("EXECUTABLE", "python.exe") - architecture = os.environ.get( - "ARCHITECTURE", - "ARM64" - if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64"), + pillow_dir = os.path.realpath(os.path.join(winbuild_dir, "..")) + + parser = argparse.ArgumentParser( + prog="winbuild\\build_prepare.py", + description="Download dependencies and generate build scripts for Pillow.", + epilog="""Arguments can also be supplied using the environment variables + PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. + See winbuild\\build.rst for more information.""", ) - build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) - cmake_generator = "Ninja" - sources_dir = "" - for arg in sys.argv[1:]: - if arg == "-v": - verbose = True - elif arg == "--nmake": - cmake_generator = "NMake Makefiles" - elif arg == "--no-imagequant": - disabled += ["libimagequant"] - elif arg == "--no-raqm" or arg == "--no-fribidi": - disabled += ["fribidi"] - elif arg.startswith("--depends="): - depends_dir = arg[10:] - elif arg.startswith("--python="): - python_dir = arg[9:] - elif arg.startswith("--executable="): - python_exe = arg[13:] - elif arg.startswith("--architecture="): - architecture = arg[15:] - elif arg.startswith("--dir="): - build_dir = arg[6:] - elif arg == "--srcdir": - sources_dir = os.path.sep + "src" - else: - msg = "Unknown parameter: " + arg - raise ValueError(msg) - - # dependency cache directory - os.makedirs(depends_dir, exist_ok=True) - print("Caching dependencies in:", depends_dir) + parser.add_argument( + "-v", "--verbose", action="store_true", help="print generated scripts" + ) + parser.add_argument( + "-d", + "--dir", + "--build-dir", + dest="build_dir", + metavar="PILLOW_BUILD", + default=os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")), + help="build directory (default: 'winbuild\\build')", + ) + parser.add_argument( + "--depends", + dest="depends_dir", + metavar="PILLOW_DEPS", + default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), + help="directory used to store cached dependencies (default: 'winbuild\\depends')", # noqa: E501 + ) + parser.add_argument( + "--architecture", + choices=architectures, + default=os.environ.get( + "ARCHITECTURE", + ( + "ARM64" + if platform.machine() == "ARM64" + else ("x86" if struct.calcsize("P") == 4 else "x64") + ), + ), + help="build architecture (default: same as host python)", + ) + parser.add_argument( + "--python", + dest="python_dir", + metavar="PYTHON", + default=os.environ.get("PYTHON"), + help="Python install directory (default: use host python)", + ) + parser.add_argument( + "--executable", + dest="python_exe", + metavar="EXECUTABLE", + default=os.environ.get("EXECUTABLE", "python.exe"), + help="Python executable (default: use host python)", + ) + parser.add_argument( + "--nmake", + dest="cmake_generator", + action="store_const", + const="NMake Makefiles", + default="Ninja", + help="build dependencies using NMake instead of Ninja", + ) + parser.add_argument( + "--no-imagequant", + action="store_true", + help="skip GPL-licensed optional dependency libimagequant", + ) + parser.add_argument( + "--no-fribidi", + "--no-raqm", + action="store_true", + help="skip LGPL-licensed optional dependency FriBiDi", + ) + args = parser.parse_args() - if python_dir is None: - python_dir = os.path.dirname(os.path.realpath(sys.executable)) - python_exe = os.path.basename(sys.executable) - print("Target Python:", os.path.join(python_dir, python_exe)) + arch_prefs = architectures[args.architecture] + print("Target Architecture:", args.architecture) - arch_prefs = architectures[architecture] - print("Target Architecture:", architecture) + if args.python_dir is None: + args.python_dir = os.path.dirname(os.path.realpath(sys.executable)) + args.python_exe = os.path.basename(sys.executable) + print("Target Python:", os.path.join(args.python_dir, args.python_exe)) msvs = find_msvs() if msvs is None: @@ -632,35 +667,47 @@ def build_pillow(): raise RuntimeError(msg) print("Found Visual Studio at:", msvs["vs_dir"]) - print("Using output directory:", build_dir) + # dependency cache directory + args.depends_dir = os.path.abspath(args.depends_dir) + os.makedirs(args.depends_dir, exist_ok=True) + print("Caching dependencies in:", args.depends_dir) + + args.build_dir = os.path.abspath(args.build_dir) + print("Using output directory:", args.build_dir) # build directory for *.h files - inc_dir = os.path.join(build_dir, "inc") + inc_dir = os.path.join(args.build_dir, "inc") # build directory for *.lib files - lib_dir = os.path.join(build_dir, "lib") + lib_dir = os.path.join(args.build_dir, "lib") # build directory for *.bin files - bin_dir = os.path.join(build_dir, "bin") + bin_dir = os.path.join(args.build_dir, "bin") # directory for storing project files - sources_dir = build_dir + sources_dir + sources_dir = os.path.join(args.build_dir, "src") # copy dependency licenses to this directory - license_dir = os.path.join(build_dir, "license") + license_dir = os.path.join(args.build_dir, "license") - shutil.rmtree(build_dir, ignore_errors=True) - os.makedirs(build_dir, exist_ok=False) + shutil.rmtree(args.build_dir, ignore_errors=True) + os.makedirs(args.build_dir, exist_ok=False) for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]: os.makedirs(path, exist_ok=True) + disabled = [] + if args.no_imagequant: + disabled += ["libimagequant"] + if args.no_fribidi: + disabled += ["fribidi"] + prefs = { # Python paths / preferences - "python_dir": python_dir, - "python_exe": python_exe, - "architecture": architecture, + "python_dir": args.python_dir, + "python_exe": args.python_exe, + "architecture": args.architecture, **arch_prefs, # Pillow paths - "pillow_dir": os.path.realpath(os.path.join(winbuild_dir, "..")), + "pillow_dir": pillow_dir, "winbuild_dir": winbuild_dir, # Build paths - "build_dir": build_dir, + "build_dir": args.build_dir, "inc_dir": inc_dir, "lib_dir": lib_dir, "bin_dir": bin_dir, @@ -669,7 +716,7 @@ def build_pillow(): # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically - "cmake_generator": cmake_generator, + "cmake_generator": args.cmake_generator, # TODO find NASM automatically # script header "header": sum([header, msvs["header"], ["@echo on"]], []), @@ -682,4 +729,6 @@ def build_pillow(): write_script(".gitignore", ["*"]) build_dep_all() + if args.verbose: + print() build_pillow() From c5e1b5ad6694aa3b67277ee09a41d3408f645ddf Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 13 Feb 2023 15:26:00 +0000 Subject: [PATCH 317/727] use consistent capitalization Co-authored-by: Hugo van Kemenade --- winbuild/build.rst | 6 +++--- winbuild/build_prepare.py | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index c13acf50d14..6beb5d655de 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -57,10 +57,10 @@ Run ``build_prepare.py`` to configure the build:: directory used to store cached dependencies (default: 'winbuild\depends') --architecture {x86,x64,ARM64} - build architecture (default: same as host python) - --python PYTHON Python install directory (default: use host python) + build architecture (default: same as host Python) + --python PYTHON Python install directory (default: use host Python) --executable EXECUTABLE - Python executable (default: use host python) + Python executable (default: use host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant --no-fribidi, --no-raqm diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index de07810a84c..359aad817bb 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -603,7 +603,8 @@ def build_pillow(): dest="depends_dir", metavar="PILLOW_DEPS", default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), - help="directory used to store cached dependencies (default: 'winbuild\\depends')", # noqa: E501 + help="directory used to store cached dependencies " + "(default: 'winbuild\\depends')", ) parser.add_argument( "--architecture", @@ -616,21 +617,21 @@ def build_pillow(): else ("x86" if struct.calcsize("P") == 4 else "x64") ), ), - help="build architecture (default: same as host python)", + help="build architecture (default: same as host Python)", ) parser.add_argument( "--python", dest="python_dir", metavar="PYTHON", default=os.environ.get("PYTHON"), - help="Python install directory (default: use host python)", + help="Python install directory (default: use host Python)", ) parser.add_argument( "--executable", dest="python_exe", metavar="EXECUTABLE", default=os.environ.get("EXECUTABLE", "python.exe"), - help="Python executable (default: use host python)", + help="Python executable (default: use host Python)", ) parser.add_argument( "--nmake", @@ -654,7 +655,7 @@ def build_pillow(): args = parser.parse_args() arch_prefs = architectures[args.architecture] - print("Target Architecture:", args.architecture) + print("Target architecture:", args.architecture) if args.python_dir is None: args.python_dir = os.path.dirname(os.path.realpath(sys.executable)) From 91a53ed28075366ca7fa6bdb3591a66f8e03c7d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 15:26:50 +0000 Subject: [PATCH 318/727] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 359aad817bb..221a77704c9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -604,7 +604,7 @@ def build_pillow(): metavar="PILLOW_DEPS", default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), help="directory used to store cached dependencies " - "(default: 'winbuild\\depends')", + "(default: 'winbuild\\depends')", ) parser.add_argument( "--architecture", From b05bc346045f4d243cdaecf85ff61567e5ca377c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 15:42:38 +1100 Subject: [PATCH 319/727] Test lists and tuples --- Tests/check_jpeg_leaks.py | 12 +-- Tests/test_imagedraw.py | 179 ++++++++++++++++++++++---------------- Tests/test_imagedraw2.py | 32 +++---- 3 files changed, 130 insertions(+), 93 deletions(-) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index ab8d7771992..5d95ca29ca7 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -110,11 +110,13 @@ def test_qtables_leak(): ) ] - qtables = [standard_l_qtable, standard_chrominance_qtable] - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) + for qtables in ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ): + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) def test_exif_leak(): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index d4723c92401..f6f27d32db8 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -27,15 +27,21 @@ Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] - -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) + +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +KITE_POINTS = ( + ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), + [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], +) def test_sanity(): @@ -63,7 +69,7 @@ def test_mode_mismatch(): ImageDraw.ImageDraw(im, mode="L") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) def test_arc(bbox, start, end): # Arrange @@ -77,7 +83,8 @@ def test_arc(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) -def test_arc_end_le_start(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_end_le_start(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -85,13 +92,14 @@ def test_arc_end_le_start(): end = 0 # Act - draw.arc(BBOX1, start=start, end=end) + draw.arc(bbox, start=start, end=end) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png") -def test_arc_no_loops(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_no_loops(bbox): # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -100,57 +108,61 @@ def test_arc_no_loops(): end = 370 # Act - draw.arc(BBOX1, start=start, end=end) + draw.arc(bbox, start=start, end=end) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) -def test_arc_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, width=5) + draw.arc(bbox, 10, 260, width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) -def test_arc_width_pieslice_large(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_pieslice_large(bbox): # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=100) + draw.arc(bbox, 10, 260, fill="yellow", width=100) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) -def test_arc_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=5) + draw.arc(bbox, 10, 260, fill="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) -def test_arc_width_non_whole_angle(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_non_whole_angle(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" # Act - draw.arc(BBOX1, 10, 259.5, width=5) + draw.arc(bbox, 10, 259.5, width=5) # Assert assert_image_similar_tofile(im, expected, 1) @@ -184,7 +196,7 @@ def test_bitmap(): @pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_chord(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -198,37 +210,40 @@ def test_chord(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_chord_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, outline="yellow", width=5) + draw.chord(bbox, 10, 260, outline="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) -def test_chord_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) -def test_chord_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0) + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") @@ -247,7 +262,7 @@ def test_chord_too_fat(): @pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -261,13 +276,14 @@ def test_ellipse(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse_translucent(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_translucent(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) + draw.ellipse(bbox, fill=(0, 255, 0, 127)) # Assert expected = "Tests/images/imagedraw_ellipse_translucent.png" @@ -297,13 +313,14 @@ def test_ellipse_symmetric(): assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) -def test_ellipse_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, outline="blue", width=5) + draw.ellipse(bbox, outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) @@ -321,25 +338,27 @@ def test_ellipse_width_large(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) -def test_ellipse_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=5) + draw.ellipse(bbox, fill="green", outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) -def test_ellipse_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=0) + draw.ellipse(bbox, fill="green", outline="blue", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") @@ -380,7 +399,7 @@ def test_ellipse_various_sizes_filled(): ) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -452,7 +471,7 @@ def test_transform(): assert_image_equal(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) def test_pieslice(bbox, start, end): # Arrange @@ -466,38 +485,41 @@ def test_pieslice(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) -def test_pieslice_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) + draw.pieslice(bbox, 10, 260, outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) -def test_pieslice_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_pieslice_width_fill.png" # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=5) # Assert assert_image_similar_tofile(im, expected, 1) -def test_pieslice_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0) + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") @@ -545,7 +567,7 @@ def test_pieslice_no_spikes(): assert_image_equal(im, im_pre_erase) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_point(points): # Arrange im = Image.new("RGB", (W, H)) @@ -558,7 +580,7 @@ def test_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -572,7 +594,8 @@ def test_polygon(points): @pytest.mark.parametrize("mode", ("RGB", "L")) -def test_polygon_kite(mode): +@pytest.mark.parametrize("kite_points", KITE_POINTS) +def test_polygon_kite(mode, kite_points): # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -581,7 +604,7 @@ def test_polygon_kite(mode): expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + draw.polygon(kite_points, fill="blue", outline="yellow") # Assert assert_image_equal_tofile(im, expected) @@ -628,7 +651,7 @@ def test_polygon_translucent(): assert_image_equal_tofile(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -655,63 +678,68 @@ def test_big_rectangle(): assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) -def test_rectangle_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width.png" # Act - draw.rectangle(BBOX1, outline="green", width=5) + draw.rectangle(bbox, outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width_fill.png" # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=5) + draw.rectangle(bbox, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=0) + draw.rectangle(bbox, fill="blue", outline="green", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") -def test_rectangle_I16(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_I16(bbox): # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="black", outline="green") + draw.rectangle(bbox, fill="black", outline="green") # Assert assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") -def test_rectangle_translucent_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_translucent_outline(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5) + draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5) # Assert assert_image_equal_tofile( @@ -758,13 +786,14 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): ) -def test_rounded_rectangle_zero_radius(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rounded_rectangle_zero_radius(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5) + draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") @@ -794,14 +823,15 @@ def test_rounded_rectangle_translucent(xy, suffix): ) -def test_floodfill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill(bbox): red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -826,13 +856,14 @@ def test_floodfill(): assert_image_equal(im, Image.new("RGB", (1, 1), red)) -def test_floodfill_border(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_border(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -847,13 +878,14 @@ def test_floodfill_border(): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_thresh(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_thresh(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="darkgreen", fill="green") + draw.rectangle(bbox, outline="darkgreen", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -1291,7 +1323,8 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.ImageFont) -def test_same_color_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_same_color_outline(bbox): # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1307,12 +1340,12 @@ def test_same_color_outline(): for mode in ["RGB", "L"]: for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: for operation, args in { - "chord": [BBOX1, 0, 180], - "ellipse": [BBOX1], + "chord": [bbox, 0, 180], + "ellipse": [bbox], "shape": [s], - "pieslice": [BBOX1, -90, 45], + "pieslice": [bbox, -90, 45], "polygon": [[(18, 30), (85, 30), (60, 72)]], - "rectangle": [BBOX1], + "rectangle": [bbox], }.items(): # Arrange im = Image.new(mode, (W, H)) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 6fc829f1a54..143341b0a65 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -27,15 +27,16 @@ Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] - -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] - -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) + +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) FONT_PATH = "Tests/fonts/FreeMono.ttf" @@ -52,7 +53,7 @@ def test_sanity(): draw.line(list(range(10)), pen) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -80,7 +81,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -94,7 +95,8 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_line_pen_as_brush(): +@pytest.mark.parametrize("points", POINTS) +def test_line_pen_as_brush(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -103,13 +105,13 @@ def test_line_pen_as_brush(): # Act # Pass in the pen as the brush parameter - draw.line(POINTS1, pen, brush) + draw.line(points, pen, brush) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -124,7 +126,7 @@ def test_polygon(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) From ad0e9dbaaf41e5c7abddf08d850cf0c0d98a3a92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Feb 2023 10:52:32 +1100 Subject: [PATCH 320/727] 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 a4481d85f47..fdabae3a38a 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 2cf5b173f3a..aaaf8fcb928 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -764,6 +764,8 @@ def load_undefined(self, data, legacy_api=True): @_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 321/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 626860e7158..aa03cfc4093 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 322/727] 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 a41ef7cf8f2..56937b5c7f6 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 323/727] 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 a41ef7cf8f2..a85ca59dd12 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 324/727] 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 | 108 ++++++++++++------ 19 files changed, 105 insertions(+), 35 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) - - if d == 0: - # If the corners have no curve, that is a rectangle + 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 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 @@ def draw_corners(pieslice): ) 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 @@ def draw_corners(pieslice): 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 325/727] 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 dc77c06ea51..a55ebbe8eeb 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -296,7 +296,7 @@ def rectangle(self, xy, fill=None, outline=None, width=1): 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 326/727] 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 4d48b822a85..0db19a64e43 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 926a2f75a06..d6b42589ad3 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 59dfac5889f..75604e17a96 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 ad0abbbd92d..976b148fc7f 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 b9bdfc50772..7c1a3ad7017 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 2ee3cadb70b..4151be4a746 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 6bfd50588ab..f31941c9abb 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 eb4304843e1..76e13b06172 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 80002b0ce71..f2e2352897a 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 a19da361aaa..616cf4aa3e2 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 6dbfa2702eb..3dfb2584094 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 71666977127..d4275a274ac 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 327/727] 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 bef8afa9d8e..35980f19cc4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ def cmd_msbuild( "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 328/727] 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 216b93ca96a..a45b22bf6ad 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 926a2f75a06..081b8496327 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 baad4939f26..d4f1ef93a45 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 329/727] 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 a45b22bf6ad..2afec99605b 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): - 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): +@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, 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 330/727] 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 d4f1ef93a45..4fa1998ba1e 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 331/727] 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 34609314cdf..f36ad5d4692 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ def __init__(self): 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 332/727] 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 306a2f1bff6..b115517acba 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 cf039e86e61..6099b04e44b 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 333/727] 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 385192a3cdc..e4c0001d193 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -177,13 +177,14 @@ def test_units(self): 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 63071b78c9c..4fd02449c7d 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -36,12 +36,10 @@ def test_warning(self): 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 @@ def test_enlarge_crop(self): # 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 @@ def test_crop_decompression_checks(self): 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 51637c78645..9f850d0e997 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 open(): 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 open_frames_zero_default(): 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 ef378b24a91..1adda772956 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 70d4d76db94..cb767a0d842 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 bce72d1927d..8f11f0a1c87 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 9c1c3cf170e..4e6dbe6ede4 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 425e690d66b..bdc704ee10f 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 f0dedc2defc..ea701f70d1a 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 036cb9d4be5..ff78993fed8 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 011e208d8d8..122690e3457 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 799c243d655..b27fa25f3e0 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 bac00e8554a..1a5730f49d9 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 70142747ca2..d0d9ed891ad 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -57,11 +57,10 @@ def test_sanity(self, tmp_path): @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 @@ def test_invalid_file(self): 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 fdabae3a38a..9a568152633 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 f1bdc59cf34..335201fe134 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -29,7 +29,9 @@ def test_unsupported(self): 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 334/727] 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 926a2f75a06..9a3ddcab77a 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 335/727] 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 d6b42589ad3..4c2af3db847 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 0aecd348366..3a9572ab221 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 976b148fc7f..0eba1141a2a 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 9aa26916a90..e325a028020 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 29ceee314cc..b27228ec924 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 3cf59c610c8..047990f1c2a 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 c85da4fb57a..044aede6264 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 516fa63a783..946bd3c4bed 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 63f88fddd21..118d988d6d4 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 f2e7d9edd28..a27b2fb4efc 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 b234b7b4efb..04d6f5dcd58 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 f9eb9b52418..ed58ca3a591 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 20a009cc177..0fb33de7579 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 0024a537d12..cb46f127cbb 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 c902ccf71fb..f11953168a0 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 9becf91465e..e61471e726f 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 e97b58a41cd..19690ca59b5 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 fec4694b290..70120031797 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 bd13c391e1d..173b2926f9c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -297,27 +297,21 @@ def getlength(self, text, mode="", direction=None, features=None, language=None) 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 336/727] 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 f36ad5d4692..4929fa93378 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ def __init__(self): 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 337/727] 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 081b8496327..597d1b64468 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 338/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa03cfc4093..d37ba4ab326 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 339/727] 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 e4c0001d193..cb6cde8ebdd 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -178,13 +178,13 @@ def test_units(self): 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 340/727] 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 e0dd4dedefe..3f7b760d6f0 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 341/727] 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 63bad83a1c3..670907c67eb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -767,12 +767,12 @@ def tobytes(self, encoder_name="raw", *args): 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 342/727] 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 132490a8e23..dfa715686e9 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 343/727] 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 d5f510f0355..1a4b8f6d98d 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,9 +86,23 @@ def __init__(self, fp, charset_encoding="iso8859-1"): 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 @@ def _load_bitmaps(self, metrics): 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 344/727] 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 aaaf8fcb928..0491a736d10 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 345/727] 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 9a568152633..b7d100e7a05 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 346/727] 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 d2f5a6d1760..3048827e038 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 1185ef2d341..1359aeb1282 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -32,7 +32,7 @@ def _open(self): 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 347/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d37ba4ab326..15decc7f06a 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 348/727] 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 7fb05cda7b2..c299e454402 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 7a9b956c559..82a3af83261 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 349/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 15decc7f06a..fe0230c344c 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 350/727] 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 670907c67eb..cf9ab2df6b1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -765,17 +765,17 @@ def tobytes(self, encoder_name="raw", *args): 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 dfa715686e9..8e4f7dfb2c8 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 1a4b8f6d98d..2300efe40b3 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,7 +86,6 @@ def __init__(self, fp, charset_encoding="iso8859-1"): for ch, ix in enumerate(encoding): if ix is not None: - ix_metrics = metrics[ix] ( xsize, ysize, @@ -96,7 +95,7 @@ def __init__(self, fp, charset_encoding="iso8859-1"): ascent, descent, attributes, - ) = ix_metrics + ) = metrics[ix] self.glyph[ch] = ( (width, 0), (left, descent - ysize, xsize + left, descent), @@ -220,9 +219,11 @@ def _load_bitmaps(self, metrics): 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 0491a736d10..04d246dd478 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 351/727] 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 3f7b760d6f0..075d462907a 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 352/727] 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 2300efe40b3..8db5822fe7d 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -219,10 +219,10 @@ def _load_bitmaps(self, metrics): 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 353/727] Release buffer --- src/decode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decode.c b/src/decode.c index 82a3af83261..7e3fadc040f 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 354/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fe0230c344c..d5798d41b02 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 355/727] 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 81e3d8f464a..9df4a5dadc2 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 356/727] 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 1b5719a8e34..55d5ee8329a 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 35980f19cc4..3a885afaf5a 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -289,9 +289,9 @@ def cmd_msbuild( # "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 357/727] Raise an error if co-ordinates are incorrectly ordered --- .../imagedraw_ellipse_various_sizes.png | Bin 21446 -> 21600 bytes ...imagedraw_ellipse_various_sizes_filled.png | Bin 20315 -> 20325 bytes Tests/test_imagedraw.py | 28 ++++++++++++++--- src/PIL/ImageDraw.py | 6 ++++ src/_imaging.c | 27 ++++++++++++++++ src/libImaging/Draw.c | 29 ++++++------------ 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png index 11a1be6faebea1a31854e4d13975f1a5c12c811e..5e3cf22b4ad8e19506b7c9551996f522bff75046 100644 GIT binary patch literal 21600 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

L*eghq9S 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 F{vQsP3wQtk literal 20315 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 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ea0711ad53c..a7267a87e73 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 a55ebbe8eeb..2d0a98765ed 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,6 +303,12 @@ def rounded_rectangle( (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 cece2e93a33..12d7f93a931 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 77343e583d4..82f290bd0cd 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 358/727] 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 a7267a87e73..5295021a37f 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 2d0a98765ed..5a0df09cb7e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,11 +303,11 @@ def rounded_rectangle( (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 12d7f93a931..1c25ab00c1d 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 359/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d5798d41b02..90f97d89f78 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 + [radarhere] + - Added "corners" argument to ImageDraw rounded_rectangle() #6954 [radarhere] From 4f9c3847e85a210e5b57269b04ef7ad6179ad698 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 2 Mar 2023 14:21:17 -0600 Subject: [PATCH 360/727] notes about %ImageData, and use better var names --- src/PIL/EpsImagePlugin.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 9da6e946bee..52036b0ac53 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -320,20 +320,32 @@ def check_required_header_comments(): elif bytes_mv[:11] == b"%ImageData:": # Check for an "ImageData" descriptor - # Encoded bitmapped image. - x, y, bi, mo = byte_arr[11:bytes_read].split(None, 7)[:4] - - if int(bi) == 1: + # Values: + # columns + # rows + # bit depth + # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) + # number of padding channels + # block size (number of bytes per row per channel) + # binary/ascii (1: binary, 2: ascii) + # data start identifier (the image data follows after a single line + # consisting only of this quoted value) + image_data_values = byte_arr[11:bytes_read].split(None, 7) + columns, rows, bit_depth, mode_id = [ + int(value) for value in image_data_values[:4] + ] + + if bit_depth == 1: self.mode = "1" - elif int(bi) == 8: + elif bit_depth == 8: try: - self.mode = self.mode_map[int(mo)] + self.mode = self.mode_map[mode_id] except ValueError: break else: break - self._size = int(x), int(y) + self._size = columns, rows return bytes_read = 0 From 60b717a94b98d4d56fb21b758330e613e5d3ae24 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 2 Mar 2023 15:26:06 -0600 Subject: [PATCH 361/727] add link to %ImageData definition and remove empty comment lines --- src/PIL/EpsImagePlugin.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 52036b0ac53..1c88d22c749 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -31,9 +31,9 @@ from ._binary import i32le as i32 from ._deprecate import deprecate -# # -------------------------------------------------------------------- + split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") @@ -319,11 +319,12 @@ def check_required_header_comments(): raise OSError(msg) elif bytes_mv[:11] == b"%ImageData:": # Check for an "ImageData" descriptor + # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 # Values: # columns # rows - # bit depth + # bit depth (1 or 8) # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) # number of padding channels # block size (number of bytes per row per channel) @@ -395,18 +396,15 @@ def load_seek(self, *args, **kwargs): pass -# # -------------------------------------------------------------------- def _save(im, fp, filename, eps=1): """EPS Writer for the Python Imaging Library.""" - # # make sure image data is available im.load() - # # determine PostScript image mode if im.mode == "L": operator = (8, 1, b"image") @@ -419,7 +417,6 @@ def _save(im, fp, filename, eps=1): raise ValueError(msg) if eps: - # # write EPS header fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") @@ -431,7 +428,6 @@ def _save(im, fp, filename, eps=1): fp.write(b"%%ImageData: %d %d " % im.size) fp.write(b'%d %d 0 1 1 "%s"\n' % operator) - # # image header fp.write(b"gsave\n") fp.write(b"10 dict begin\n") @@ -452,7 +448,6 @@ def _save(im, fp, filename, eps=1): fp.flush() -# # -------------------------------------------------------------------- From 1a790a91f57ac764d77fb8e5d23b82669419e391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Mar 2023 14:38:51 +1100 Subject: [PATCH 362/727] 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 3a885afaf5a..2820bdb36cf 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ def cmd_msbuild( "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 363/727] 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 1adda772956..22686af3485 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 cb767a0d842..f96afdc95be 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 8f11f0a1c87..8522f486aff 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 bdc704ee10f..fd00f260e78 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 ea701f70d1a..2e921e46701 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 ff78993fed8..e405834b5ca 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 122690e3457..09f1ef8e4a6 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 d0d9ed891ad..b40f690f557 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -57,10 +57,12 @@ def test_sanity(self, tmp_path): @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 364/727] 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 9f850d0e997..b2bec598496 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 365/727] 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 04d246dd478..42038831cc5 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 366/727] 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 cb6cde8ebdd..9021a9fb36d 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -179,11 +179,11 @@ def test_units(self): @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 335201fe134..a7b6c735a69 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -31,7 +31,8 @@ def test_unsupported(self): 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 367/727] 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 f886d3aae99..871ad28b579 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1065,3 +1065,9 @@ def test_save_zero(self, compression, tmp_path): 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 368/727] 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 9b5a121d7d3..efb045667c9 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 369/727] Added memoryview support to Dib.frombytes() --- Tests/test_imagewin.py | 13 ++++++++----- src/display.c | 10 +++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 9d64d17a30f..5e489284f2f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -100,8 +100,11 @@ def test_dib_frombytes_tobytes_roundtrip(self): # Act # Make one the same as the using tobytes()/frombytes() test_buffer = dib1.tobytes() - dib2.frombytes(test_buffer) - - # Assert - # Confirm they're the same - assert dib1.tobytes() == dib2.tobytes() + 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() diff --git a/src/display.c b/src/display.c index a50fc3e242b..227e306a109 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 370/727] 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 9df4a5dadc2..9565ab149b8 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 00000000000..c063d638e06 --- /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 a2b58869696..177fb65dd08 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 371/727] 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 c063d638e06..df2ec53fa09 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 372/727] 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 45118143474..6c9ed66e32b 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 7d2b20d65cc..f7153386ee6 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 737da7b94c7..ddfafc9d7f4 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 306e34ca91b..833f096c310 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 33958bea85e..10c3cd929f8 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 373/727] 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 00000000000..2ae14d468b4 --- /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 374/727] 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 f8b050f7625..6fab0ecd227 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 375/727] 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 55d5ee8329a..f0aa0f399cf 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 376/727] 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 f0aa0f399cf..98957335b13 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 377/727] 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 227e306a109..e8e7b62c2e5 100644 --- a/src/display.c +++ b/src/display.c @@ -202,6 +202,7 @@ _frombytes(ImagingDisplayObject *display, PyObject *args) { } if (display->dib->ysize * display->dib->linesize != buffer.len) { + PyBuffer_Release(&buffer); PyErr_SetString(PyExc_ValueError, "wrong size"); return NULL; } From 1690592d8b514e4580384a6ca297540654a5f04b Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 6 Mar 2023 02:24:00 +0000 Subject: [PATCH 378/727] correct minimum CMake version --- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 67aac597bc3..21b40d4e671 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -10,7 +10,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. -* Requires CMake 3.13 or newer (available as Visual Studio component). +* Requires CMake 3.15 or newer (available as Visual Studio component). * Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). diff --git a/winbuild/build.rst b/winbuild/build.rst index 884177d8c3e..e83045f0cf8 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -21,7 +21,7 @@ Download and install: `_ (MSVC C++ build tools, and any Windows SDK version required) -* `CMake 3.13 or newer `_ +* `CMake 3.15 or newer `_ (also available as Visual Studio component C++ CMake tools for Windows) * `Ninja `_ From 29b6db4f8a271b0c90d2bf129b40f2f3ca65185d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 6 Mar 2023 10:26:30 +0200 Subject: [PATCH 379/727] 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 2ae14d468b4..8a326547684 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 380/727] 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 db030704607..560d6c7dfea 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -3,10 +3,12 @@ name: CIFuzz on: push: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" workflow_dispatch: @@ -14,7 +16,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 459f0d8352f1bf0394e99b19fd09d529aa928d73 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Mar 2023 18:07:46 +1100 Subject: [PATCH 381/727] Round duration when saving animated WebP --- Tests/test_file_webp_animated.py | 12 ++++++++++++ src/PIL/WebPImagePlugin.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index c621df0d99c..2fd5e548406 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -134,6 +134,18 @@ def test_timestamp_and_duration(tmp_path): ts += durations[frame] +def test_float_duration(tmp_path): + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/iss634.apng") as im: + assert im.info["duration"] == 70.0 + + im.save(temp_file, save_all=True) + + with Image.open(temp_file) as reloaded: + reloaded.load() + assert reloaded.info["duration"] == 70 + + def test_seeking(tmp_path): """ Create an animated WebP file, and then try seeking through frames in reverse-order, diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index d060dd4b819..ce8e05fcbb1 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -285,7 +285,7 @@ def _save_all(im, fp, filename): # Append the frame to the animation encoder enc.add( frame.tobytes("raw", rawmode), - timestamp, + round(timestamp), frame.size[0], frame.size[1], rawmode, @@ -305,7 +305,7 @@ def _save_all(im, fp, filename): im.seek(cur_idx) # Force encoder to flush frames - enc.add(None, timestamp, 0, 0, "", lossless, quality, 0) + enc.add(None, round(timestamp), 0, 0, "", lossless, quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) From 2d01875e7c9be1b2a550f143c542bd931fa7c821 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 13:34:44 +1100 Subject: [PATCH 382/727] 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 00000000000..d5c2f392635 --- /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 02224af34bd..27a62454503 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 df2ec53fa09..ebefc3df7e0 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 00000000000..09aa1a1a4e6 --- /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 0e6f82092e4..32d2381f3c2 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -59,6 +59,7 @@ "PngImagePlugin", "PpmImagePlugin", "PsdImagePlugin", + "QoiImagePlugin", "SgiImagePlugin", "SpiderImagePlugin", "SunImagePlugin", From cbde5b262653f1b5273735eab1b63e50fdfadda7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 13:36:22 +1100 Subject: [PATCH 383/727] Added reading of JPEG2000 comments --- Tests/images/comment.jp2 | Bin 0 -> 209 bytes Tests/test_file_jpeg2k.py | 11 +++++++++++ docs/releasenotes/9.5.0.rst | 6 ++++++ src/PIL/Jpeg2KImagePlugin.py | 24 ++++++++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 Tests/images/comment.jp2 diff --git a/Tests/images/comment.jp2 b/Tests/images/comment.jp2 new file mode 100644 index 0000000000000000000000000000000000000000..4bdf91760e12035fd2c9146ba2a2654c04ffe50b GIT binary patch literal 209 zcmZQzVBpCLP*C9IYUg5LU=T?wsVvAUFj4@r8KAT-kj?;d#WFKeihwjD7&Ef718D{Z z{^b0eB9IURgCG#M02L?y_x~Trpa~X(Cw{^ JIN1N+1ONn(CH(*Z literal 0 HcmV?d00001 diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index de622c47897..81795d54c91 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -353,6 +353,17 @@ def test_subsampling_decode(name): assert_image_similar(im, expected, epsilon) +def test_comment(): + with Image.open("Tests/images/comment.jp2") as im: + assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" + + # Test an image that is truncated partway through a codestream + with open("Tests/images/comment.jp2", "rb") as fp: + b = BytesIO(fp.read(130)) + with Image.open(b) as im: + pass + + @pytest.mark.parametrize( "test_file", [ diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index df2ec53fa09..e9d35aee25d 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -43,6 +43,12 @@ Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` ``corners``. This a tuple of Booleans, specifying whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. +Reading JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. + Security ======== diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 7457874c1ec..1dec2f84aa1 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -218,6 +218,8 @@ def _open(self): self._size, self.mode, self.custom_mimetype, dpi = header if dpi is not None: self.info["dpi"] = dpi + if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + self._parse_comment() else: msg = "not a JPEG 2000 file" raise SyntaxError(msg) @@ -254,6 +256,28 @@ def _open(self): ) ] + def _parse_comment(self): + hdr = self.fp.read(2) + length = struct.unpack(">H", hdr)[0] + self.fp.seek(length - 2, os.SEEK_CUR) + + while True: + marker = self.fp.read(2) + if not marker: + break + typ = marker[1] + if typ in (0x90, 0xD9): + # Start of tile or end of codestream + break + hdr = self.fp.read(2) + length = struct.unpack(">H", hdr)[0] + if typ == 0x64: + # Comment + self.info["comment"] = self.fp.read(length - 2)[2:] + break + else: + self.fp.seek(length - 2, os.SEEK_CUR) + @property def reduce(self): # https://github.com/python-pillow/Pillow/issues/4343 found that the From 69325629742a08989424af5e729aaafea8eb6118 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 22:21:37 +1100 Subject: [PATCH 384/727] 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 42038831cc5..8c0431492fa 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -425,6 +425,9 @@ def delegate(self, *args): __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") + # Python >= 3.11 + if hasattr(Fraction, "__int__"): + __int__ = _delegate("__int__") class ImageFileDirectory_v2(MutableMapping): From 929dbba834ccbaae1a99c28388e3c223ed9df88f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Mar 2023 17:37:06 +1100 Subject: [PATCH 385/727] Handle failure from PyDict_New or PyList_New --- src/_imaging.c | 6 ++++++ src/_imagingft.c | 9 +++++++++ src/_imagingmorph.c | 7 ++++++- src/path.c | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 1c25ab00c1d..dc14361f626 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1249,6 +1249,9 @@ _histogram(ImagingObject *self, PyObject *args) { /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); + if (list == NULL) { + return NULL; + } for (i = 0; i < h->bands * 256; i++) { PyObject *item; item = PyLong_FromLong(h->histogram[i]); @@ -2154,6 +2157,9 @@ _getcolors(ImagingObject *self, PyObject *args) { Py_INCREF(out); } else { out = PyList_New(colors); + if (out == NULL) { + return NULL; + } for (i = 0; i < colors; i++) { ImagingColorItem *v = &items[i]; PyObject *item = Py_BuildValue( diff --git a/src/_imagingft.c b/src/_imagingft.c index 0db17a5a6db..3e5244b2f03 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1082,6 +1082,9 @@ font_getvarnames(FontObject *self) { num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); + if (list_names == NULL) { + return NULL; + } name_count = FT_Get_Sfnt_Name_Count(self->face); for (i = 0; i < name_count; i++) { @@ -1125,10 +1128,16 @@ font_getvaraxes(FontObject *self) { name_count = FT_Get_Sfnt_Name_Count(self->face); list_axes = PyList_New(num_axis); + if (list_axes == NULL) { + return NULL; + } for (i = 0; i < num_axis; i++) { axis = master->axis[i]; list_axis = PyDict_New(); + if (list_axis == NULL) { + return NULL; + } PyDict_SetItemString( list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c0644b61609..43b72539df0 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -136,6 +136,9 @@ match(PyObject *self, PyObject *args) { int row_idx, col_idx; UINT8 **inrows; PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); @@ -213,10 +216,12 @@ get_on_pixels(PyObject *self, PyObject *args) { int row_idx, col_idx; int width, height; PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } if (!PyArg_ParseTuple(args, "n", &i0)) { PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; } img = (Imaging)i0; diff --git a/src/path.c b/src/path.c index 3e3431575ec..e17580fa227 100644 --- a/src/path.c +++ b/src/path.c @@ -439,6 +439,9 @@ path_tolist(PyPathObject *self, PyObject *args) { if (flat) { list = PyList_New(self->count * 2); + if (list == NULL) { + return NULL; + } for (i = 0; i < self->count * 2; i++) { PyObject *item; item = PyFloat_FromDouble(self->xy[i]); @@ -449,6 +452,9 @@ path_tolist(PyPathObject *self, PyObject *args) { } } else { list = PyList_New(self->count); + if (list == NULL) { + return NULL; + } for (i = 0; i < self->count; i++) { PyObject *item; item = Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); From a334bb6524d38916e39fad44e920ce91cc43eea4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Mar 2023 20:24:34 +1100 Subject: [PATCH 386/727] 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 8b847b8943d..362ad95a2db 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 98957335b13..1d38919b19c 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 387/727] 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 493e0709c46..e8d01f7b288 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -955,6 +955,13 @@ addTransparencyFlagToModule(PyObject *m) { static int setup_module(PyObject *m) { +#ifdef HAVE_WEBPANIM + /* Ready object types */ + if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || + PyType_Ready(&WebPAnimEncoder_Type) < 0) { + return -1; + } +#endif PyObject *d = PyModule_GetDict(m); addMuxFlagToModule(m); addAnimFlagToModule(m); @@ -963,13 +970,6 @@ setup_module(PyObject *m) { PyDict_SetItemString( d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); -#ifdef HAVE_WEBPANIM - /* Ready object types */ - if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || - PyType_Ready(&WebPAnimEncoder_Type) < 0) { - return -1; - } -#endif return 0; } From c63b0ca2106d452311b201f52d4ecb107ffe0c31 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Mar 2023 18:32:43 +1100 Subject: [PATCH 388/727] Decrement reference count --- src/_imaging.c | 66 ++++++++++++++++++++++++++++++--------------- src/_imagingcms.c | 3 +++ src/_imagingft.c | 20 ++++++++++---- src/_imagingmorph.c | 6 ++++- src/_webp.c | 11 +++++--- 5 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 1c25ab00c1d..847eed5cebe 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3820,15 +3820,29 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } - PyDict_SetItemString(d, "new_count", PyLong_FromLong(arena->stats_new_count)); - PyDict_SetItemString( - d, "allocated_blocks", PyLong_FromLong(arena->stats_allocated_blocks)); - PyDict_SetItemString( - d, "reused_blocks", PyLong_FromLong(arena->stats_reused_blocks)); - PyDict_SetItemString( - d, "reallocated_blocks", PyLong_FromLong(arena->stats_reallocated_blocks)); - PyDict_SetItemString(d, "freed_blocks", PyLong_FromLong(arena->stats_freed_blocks)); - PyDict_SetItemString(d, "blocks_cached", PyLong_FromLong(arena->blocks_cached)); + PyObject *new_count = PyLong_FromLong(arena->stats_new_count); + PyDict_SetItemString(d, "new_count", new_count); + Py_XDECREF(new_count); + + PyObject *allocated_blocks = PyLong_FromLong(arena->stats_allocated_blocks); + PyDict_SetItemString(d, "allocated_blocks", allocated_blocks); + Py_XDECREF(allocated_blocks); + + PyObject *reused_blocks = PyLong_FromLong(arena->stats_reused_blocks); + PyDict_SetItemString(d, "reused_blocks", reused_blocks); + Py_XDECREF(reused_blocks); + + PyObject *reallocated_blocks = PyLong_FromLong(arena->stats_reallocated_blocks); + PyDict_SetItemString(d, "reallocated_blocks", reallocated_blocks); + Py_XDECREF(reallocated_blocks); + + PyObject *freed_blocks = PyLong_FromLong(arena->stats_freed_blocks); + PyDict_SetItemString(d, "freed_blocks", freed_blocks); + Py_XDECREF(freed_blocks); + + PyObject *blocks_cached = PyLong_FromLong(arena->blocks_cached); + PyDict_SetItemString(d, "blocks_cached", blocks_cached); + Py_XDECREF(blocks_cached); return d; } @@ -4197,16 +4211,18 @@ setup_module(PyObject *m) { #ifdef HAVE_LIBJPEG { extern const char *ImagingJpegVersion(void); - PyDict_SetItemString( - d, "jpeglib_version", PyUnicode_FromString(ImagingJpegVersion())); + PyObject *jpeglib_version = PyUnicode_FromString(ImagingJpegVersion()); + PyDict_SetItemString(d, "jpeglib_version", jpeglib_version); + Py_DECREF(jpeglib_version); } #endif #ifdef HAVE_OPENJPEG { extern const char *ImagingJpeg2KVersion(void); - PyDict_SetItemString( - d, "jp2klib_version", PyUnicode_FromString(ImagingJpeg2KVersion())); + PyObject *jp2klib_version = PyUnicode_FromString(ImagingJpeg2KVersion()); + PyDict_SetItemString(d, "jp2klib_version", jp2klib_version); + Py_DECREF(jp2klib_version); } #endif @@ -4215,8 +4231,9 @@ setup_module(PyObject *m) { have_libjpegturbo = Py_True; #define tostr1(a) #a #define tostr(a) tostr1(a) - PyDict_SetItemString( - d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION))); + PyObject *libjpeg_turbo_version = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); + PyDict_SetItemString(d, "libjpeg_turbo_version", libjpeg_turbo_version); + Py_DECREF(libjpeg_turbo_version); #undef tostr #undef tostr1 #else @@ -4230,8 +4247,9 @@ setup_module(PyObject *m) { have_libimagequant = Py_True; { extern const char *ImagingImageQuantVersion(void); - PyDict_SetItemString( - d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion())); + PyObject *imagequant_version = PyUnicode_FromString(ImagingImageQuantVersion()); + PyDict_SetItemString(d, "imagequant_version", imagequant_version); + Py_DECREF(imagequant_version); } #else have_libimagequant = Py_False; @@ -4248,16 +4266,18 @@ setup_module(PyObject *m) { PyModule_AddIntConstant(m, "FIXED", Z_FIXED); { extern const char *ImagingZipVersion(void); - PyDict_SetItemString( - d, "zlib_version", PyUnicode_FromString(ImagingZipVersion())); + PyObject *zlibversion = PyUnicode_FromString(ImagingZipVersion()); + PyDict_SetItemString(d, "zlib_version", zlibversion); + Py_DECREF(zlibversion); } #endif #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); - PyDict_SetItemString( - d, "libtiff_version", PyUnicode_FromString(ImagingTiffVersion())); + PyObject *libtiff_version = PyUnicode_FromString(ImagingTiffVersion()); + PyDict_SetItemString(d, "libtiff_version", libtiff_version); + Py_DECREF(libtiff_version); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4280,7 +4300,9 @@ setup_module(PyObject *m) { Py_INCREF(have_xcb); PyModule_AddObject(m, "HAVE_XCB", have_xcb); - PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(version)); + PyObject *pillow_version = PyUnicode_FromString(version); + PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version); + Py_DECREF(pillow_version); return 0; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index efb045667c9..779f31b9cf6 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -950,6 +950,8 @@ _is_intent_supported(CmsProfileObject *self, int clut) { return Py_None; } PyDict_SetItem(result, id, entry); + Py_DECREF(id); + Py_DECREF(entry); } return result; } @@ -1532,6 +1534,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } PyDict_SetItemString(d, "littlecms_version", v); + Py_DECREF(v); return 0; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 0db17a5a6db..8697a74ff53 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1129,11 +1129,17 @@ font_getvaraxes(FontObject *self) { axis = master->axis[i]; list_axis = PyDict_New(); - PyDict_SetItemString( - list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); - PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); - PyDict_SetItemString( - list_axis, "maximum", PyLong_FromLong(axis.maximum / 65536)); + PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); + PyDict_SetItemString(list_axis, "minimum", minimum); + Py_XDECREF(minimum); + + PyObject *def = PyLong_FromLong(axis.def / 65536); + PyDict_SetItemString(list_axis, "default", def); + Py_XDECREF(def); + + PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); + PyDict_SetItemString(list_axis, "maximum", maximum); + Py_XDECREF(maximum); for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); @@ -1144,6 +1150,7 @@ font_getvaraxes(FontObject *self) { if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); PyDict_SetItemString(list_axis, "name", axis_name); + Py_XDECREF(axis_name); break; } } @@ -1359,6 +1366,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); PyDict_SetItemString(d, "freetype2_version", v); + Py_DECREF(v); #ifdef HAVE_RAQM #if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) @@ -1376,6 +1384,7 @@ setup_module(PyObject *m) { PyDict_SetItemString(d, "HAVE_RAQM", v); PyDict_SetItemString(d, "HAVE_FRIBIDI", v); PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); + Py_DECREF(v); if (have_raqm) { #ifdef RAQM_VERSION_MAJOR v = PyUnicode_FromString(raqm_version_string()); @@ -1383,6 +1392,7 @@ setup_module(PyObject *m) { v = Py_None; #endif PyDict_SetItemString(d, "raqm_version", v); + Py_DECREF(v); #ifdef FRIBIDI_MAJOR_VERSION { diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c0644b61609..46a40e96daf 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -194,6 +194,7 @@ match(PyObject *self, PyObject *args) { if (lut[lut_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); } } } @@ -230,6 +231,7 @@ get_on_pixels(PyObject *self, PyObject *args) { if (row[col_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); } } } @@ -240,7 +242,9 @@ static int setup_module(PyObject *m) { PyObject *d = PyModule_GetDict(m); - PyDict_SetItemString(d, "__version", PyUnicode_FromString("0.1")); + PyObject *version = PyUnicode_FromString("0.1"); + PyDict_SetItemString(d, "__version", version); + Py_DECREF(version); return 0; } diff --git a/src/_webp.c b/src/_webp.c index 493e0709c46..5575e04f9ce 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -949,8 +949,10 @@ addAnimFlagToModule(PyObject *m) { void addTransparencyFlagToModule(PyObject *m) { - PyModule_AddObject( - m, "HAVE_TRANSPARENCY", PyBool_FromLong(!WebPDecoderBuggyAlpha())); + PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); + if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { + Py_DECREF(have_transparency); + } } static int @@ -960,8 +962,9 @@ setup_module(PyObject *m) { addAnimFlagToModule(m); addTransparencyFlagToModule(m); - PyDict_SetItemString( - d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); + PyObject *webpdecoder_version = PyUnicode_FromString(WebPDecoderVersion_str()); + PyDict_SetItemString(d, "webpdecoder_version", webpdecoder_version); + Py_DECREF(webpdecoder_version); #ifdef HAVE_WEBPANIM /* Ready object types */ From 096a8ea99e485c1ac264be9ade814b0e1400fd24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Mar 2023 22:39:11 +1100 Subject: [PATCH 389/727] 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 76f185b9adc..a7714c92cc8 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -56,6 +56,7 @@ def open(self, im): 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 3048827e038..6f988729f9f 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -60,6 +60,7 @@ def open(self, im): 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 768ac12bd79..dd1c5e7d281 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -56,6 +56,7 @@ def open(self, im): 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 98dc5443cc3..7ca10fac5dd 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -57,6 +57,7 @@ def open(self, im): 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 3e147a9efec..eda485cf6de 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 390/727] 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 fa22e90f660..9a1784d31a7 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 464d138e2af..2588d3a0574 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 d145d01f724..2450c67e9a6 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -235,6 +235,14 @@ def load(self): 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 8dd9f2909cc..58f7327bde4 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -89,6 +89,14 @@ def seek(self, frame): def tell(self): return self.frame + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- From b3d782374069d18551d2947008210ab8c53ff072 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Mar 2023 00:11:48 +1100 Subject: [PATCH 391/727] Decrement reference count --- src/_imaging.c | 1 + src/_imagingft.c | 12 ++++++++++++ src/_imagingmorph.c | 5 +++++ src/_imagingtk.c | 6 +++++- src/_webp.c | 1 + 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index dc14361f626..3cc94328622 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4306,6 +4306,7 @@ PyInit__imaging(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 3e5244b2f03..93866ec4da0 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1090,6 +1090,7 @@ font_getvarnames(FontObject *self) { for (i = 0; i < name_count; i++) { error = FT_Get_Sfnt_Name(self->face, i, &name); if (error) { + Py_DECREF(list_names); return geterror(error); } @@ -1136,6 +1137,11 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); if (list_axis == NULL) { + for (j = 0; j < i; j++) { + list_axis = PyList_GetItem(list_axes, j); + Py_DECREF(list_axis); + } + Py_DECREF(list_axes); return NULL; } PyDict_SetItemString( @@ -1147,6 +1153,12 @@ font_getvaraxes(FontObject *self) { for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); if (error) { + Py_DECREF(list_axis); + for (j = 0; j < i; j++) { + list_axis = PyList_GetItem(list_axes, j); + Py_DECREF(list_axis); + } + Py_DECREF(list_axes); return geterror(error); } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 43b72539df0..3e0c9172a0a 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -141,11 +141,13 @@ match(PyObject *self, PyObject *args) { } if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } if (!PyBytes_Check(py_lut)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); return NULL; } @@ -153,6 +155,7 @@ match(PyObject *self, PyObject *args) { lut_len = PyBytes_Size(py_lut); if (lut_len < LUT_SIZE) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } @@ -161,6 +164,7 @@ match(PyObject *self, PyObject *args) { imgin = (Imaging)i0; if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } @@ -221,6 +225,7 @@ get_on_pixels(PyObject *self, PyObject *args) { } if (!PyArg_ParseTuple(args, "n", &i0)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index b9273b0b882..ac6a2313872 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -58,5 +58,9 @@ PyInit__imagingtk(void) { }; PyObject *m; m = PyModule_Create(&module_def); - return (load_tkinter_funcs() == 0) ? m : NULL; + if (load_tkinter_funcs() != 0) { + Py_DECREF(m); + return NULL;; + } + return m; } diff --git a/src/_webp.c b/src/_webp.c index 493e0709c46..0e38453cb7f 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -987,6 +987,7 @@ PyInit__webp(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } From 2fc7cfb6b256ef21ee7991604e972fd9e5e95efe Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Mar 2023 22:32:38 +1100 Subject: [PATCH 392/727] 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 21af32db644..7a94c0302f9 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -985,7 +985,7 @@ def test_open_missing_samplesperpixel(self): 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 393/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 90f97d89f78..cbf91baff0b 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 394/727] 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 0c8b390dec5..19bbf85981b 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 395/727] 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 27a62454503..5ab484df256 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 ebefc3df7e0..7bab9fed76e 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 396/727] 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 d5c2f392635..f33eada6110 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -5,24 +5,24 @@ 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 397/727] 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 df2ec53fa09..19c7b3be34b 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 398/727] 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 09aa1a1a4e6..ef91b90abca 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 399/727] 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 02224af34bd..6ac56dc305f 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 400/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cbf91baff0b..f0f3044a320 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added QOI reading #6852 + [radarhere, hugovk] + +- Added saving RGBA images as PDFs #6925 + [radarhere] + +- Do not raise an error if os.environ does not contain PATH #6935 + [radarhere, hugovk] + - Close OleFileIO instance when closing or exiting FPX or MIC #7005 [radarhere] From 023d4349e43b4c0965ddee65e481b7322f715412 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 17:00:07 +1100 Subject: [PATCH 401/727] Added release notes for #6834 --- docs/releasenotes/9.5.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index bd6e586932a..13c99313a22 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -63,3 +63,10 @@ Added support for saving PDFs in RGBA mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using the JPXDecode filter, PDFs can now be saved in RGBA mode. + + +Improved I;16N support +^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for I;16N access, packing and unpacking. Conversion to +and from L mode has also been added. From 9e6ae98362c174febada3f8a06d1965524451cf8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 17:31:12 +1100 Subject: [PATCH 402/727] Dropped support for BGR;32 mode --- Tests/test_image.py | 2 +- docs/handbook/concepts.rst | 1 - src/PIL/ImageMode.py | 1 - src/libImaging/Storage.c | 8 -------- 4 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 85e3ff55b4d..c226285099e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -58,7 +58,7 @@ def test_image_modes_success(self, mode): Image.new(mode, (1, 1)) @pytest.mark.parametrize( - "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32") + "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24") ) def test_image_modes_fail(self, mode): with pytest.raises(ValueError) as e: diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 0aa2f1119f6..e40ed4687af 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -62,7 +62,6 @@ Pillow also provides limited support for a few additional modes, including: * ``BGR;15`` (15-bit reversed true colour) * ``BGR;16`` (16-bit reversed true colour) * ``BGR;24`` (24-bit reversed true colour) - * ``BGR;32`` (32-bit reversed true colour) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 0973536c934..8b1506e9bcc 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -61,7 +61,6 @@ def getmode(mode): "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"), "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"), "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"), - "BGR;32": ("RGB", "L", ("B", "G", "R"), endian + "u4"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 76750aaf7f2..7730b4be823 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -152,14 +152,6 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; - } else if (strcmp(mode, "BGR;32") == 0) { - /* EXPERIMENTAL */ - /* 32-bit reversed true colour */ - im->bands = 1; - im->pixelsize = 4; - im->linesize = (xsize * 4 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; From 31669013d420c2a532f5be6c5631827fe36e6f70 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 23:13:34 +1100 Subject: [PATCH 403/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f0f3044a320..d7b5b4daba3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Improved I;16N support #6834 + [radarhere] + - Added QOI reading #6852 [radarhere, hugovk] From 1f1ab16631724f51473872900246604d5366b059 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 14 Mar 2023 22:35:26 +0200 Subject: [PATCH 404/727] Remove EOL Debian 10 Buster from CI --- .github/workflows/test-docker.yml | 1 - docs/installation.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index f7153386ee6..ff1605ac783 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -37,7 +37,6 @@ jobs: centos-7-amd64, centos-stream-8-amd64, centos-stream-9-amd64, - debian-10-buster-x86, debian-11-bullseye-x86, fedora-36-amd64, fedora-37-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index 1d38919b19c..6164a638bc5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -432,8 +432,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 10 Buster | 3.7 | x86 | -+----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ | Fedora 36 | 3.10 | x86-64 | From b168ec2606353af97302c1cc69d119cfe0103b7c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Mar 2023 18:07:14 +0200 Subject: [PATCH 405/727] Replace dependency sphinx-issues with builtin sphinx.ext.extlinks --- docs/Makefile | 2 +- docs/conf.py | 15 ++++++++++++--- docs/deprecations.rst | 2 +- docs/releasenotes/3.1.1.rst | 6 +++--- docs/releasenotes/3.1.2.rst | 2 +- docs/releasenotes/6.2.0.rst | 2 +- docs/releasenotes/6.2.2.rst | 8 ++++---- docs/releasenotes/7.1.0.rst | 10 +++++----- docs/releasenotes/8.0.1.rst | 2 +- docs/releasenotes/8.1.0.rst | 8 ++++---- docs/releasenotes/8.1.1.rst | 10 +++++----- docs/releasenotes/8.1.2.rst | 4 ++-- docs/releasenotes/8.2.0.rst | 20 ++++++++++---------- docs/releasenotes/8.3.0.rst | 2 +- docs/releasenotes/8.3.2.rst | 2 +- docs/releasenotes/9.0.0.rst | 6 +++--- docs/releasenotes/9.0.1.rst | 4 ++-- docs/releasenotes/9.1.1.rst | 2 +- setup.cfg | 1 - 19 files changed, 58 insertions(+), 50 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index d32d25a3c49..4a3de67cb26 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -45,7 +45,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph .PHONY: html html: diff --git a/docs/conf.py b/docs/conf.py index e1ffa49b8b2..683ff7856cb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,10 +29,10 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", + "sphinx.ext.extlinks", "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_inline_tabs", - "sphinx_issues", "sphinx_removed_in", "sphinxext.opengraph", ] @@ -317,8 +317,17 @@ def setup(app): app.add_css_file("css/dark.css") -# GitHub repo for sphinx-issues -issues_github_path = "python-pillow/Pillow" +# sphinx.ext.extlinks +# This config is a dictionary of external sites, +# mapping unique short aliases to a base URL and a prefix. +# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html +_repo = "https://github.com/python-pillow/Pillow/" +extlinks = { + "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"), + "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), + "issue": (_repo + "issues/%s", "#%s"), + "pr": (_repo + "pull/%s", "#%s"), +} # sphinxext.opengraph ogp_image = ( diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0db19a64e43..a9c6d1f7e7f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -261,7 +261,7 @@ FreeType 2.7 Support for FreeType 2.7 has been removed. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _FreeType: https://freetype.org/ diff --git a/docs/releasenotes/3.1.1.rst b/docs/releasenotes/3.1.1.rst index 38118ea39c4..5d60e116cc1 100644 --- a/docs/releasenotes/3.1.1.rst +++ b/docs/releasenotes/3.1.1.rst @@ -6,7 +6,7 @@ CVE-2016-0740 -- Buffer overflow in TiffDecode.c ------------------------------------------------ Pillow 3.1.0 and earlier when linked against libtiff >= 4.0.0 on x64 -may overflow a buffer when reading a specially crafted tiff file (:cve:`CVE-2016-0740`). +may overflow a buffer when reading a specially crafted tiff file (:cve:`2016-0740`). Specifically, libtiff >= 4.0.0 changed the return type of ``TIFFScanlineSize`` from ``int32`` to machine dependent @@ -24,7 +24,7 @@ CVE-2016-0775 -- Buffer overflow in FliDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, FliDecode.c has a buffer overflow error (:cve:`CVE-2016-0775`). +release, FliDecode.c has a buffer overflow error (:cve:`2016-0775`). Around line 192: @@ -53,7 +53,7 @@ CVE-2016-2533 -- Buffer overflow in PcdDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, ``PcdDecode.c`` has a buffer overflow error (:cve:`CVE-2016-2533`). +release, ``PcdDecode.c`` has a buffer overflow error (:cve:`2016-2533`). The ``state.buffer`` for ``PcdDecode.c`` is allocated based on a 3 bytes per pixel sizing, where ``PcdDecode.c`` wrote into the buffer diff --git a/docs/releasenotes/3.1.2.rst b/docs/releasenotes/3.1.2.rst index b5f7cfe9963..04325ad868f 100644 --- a/docs/releasenotes/3.1.2.rst +++ b/docs/releasenotes/3.1.2.rst @@ -7,7 +7,7 @@ CVE-2016-3076 -- Buffer overflow in Jpeg2KEncode.c Pillow between 2.5.0 and 3.1.1 may overflow a buffer when writing large Jpeg2000 files, allowing for code execution or other memory -corruption (:cve:`CVE-2016-3076`). +corruption (:cve:`2016-3076`). This occurs specifically in the function ``j2k_encode_entry``, at the line: diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 0fb33de7579..7daac1b1902 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -69,7 +69,7 @@ Security ======== This release catches several buffer overruns, as well as addressing -:cve:`CVE-2019-16865`. The CVE is regarding DOS problems, such as consuming large +:cve:`2019-16865`. The CVE is regarding DOS problems, such as consuming large amounts of memory, or taking a large amount of time to process an image. In RawDecode.c, an error is now thrown if skip is calculated to be less than diff --git a/docs/releasenotes/6.2.2.rst b/docs/releasenotes/6.2.2.rst index 79d4b88aac4..47692a3de6a 100644 --- a/docs/releasenotes/6.2.2.rst +++ b/docs/releasenotes/6.2.2.rst @@ -6,13 +6,13 @@ Security This release addresses several security problems. -:cve:`CVE-2019-19911` is regarding FPX images. If an image reports that it has a large +:cve:`2019-19911` is regarding FPX images. If an image reports that it has a large number of bands, a large amount of resources will be used when trying to process the image. This is fixed by limiting the number of bands to those usable by Pillow. -Buffer overruns were found when processing an SGI (:cve:`CVE-2020-5311`), -PCX (:cve:`CVE-2020-5312`) or FLI image (:cve:`CVE-2020-5313`). Checks have been added +Buffer overruns were found when processing an SGI (:cve:`2020-5311`), +PCX (:cve:`2020-5312`) or FLI image (:cve:`2020-5313`). Checks have been added to prevent this. -:cve:`CVE-2020-5310`: Overflow checks have been added when calculating the size of a +:cve:`2020-5310`: Overflow checks have been added when calculating the size of a memory block to be reallocated in the processing of a TIFF image. diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index cb46f127cbb..6e231464e93 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -72,11 +72,11 @@ Security This release includes security fixes. -* :cve:`CVE-2020-10177` Fix multiple out-of-bounds reads in FLI decoding -* :cve:`CVE-2020-10378` Fix bounds overflow in PCX decoding -* :cve:`CVE-2020-10379` Fix two buffer overflows in TIFF decoding -* :cve:`CVE-2020-10994` Fix bounds overflow in JPEG 2000 decoding -* :cve:`CVE-2020-11538` Fix buffer overflow in SGI-RLE decoding +* :cve:`2020-10177` Fix multiple out-of-bounds reads in FLI decoding +* :cve:`2020-10378` Fix bounds overflow in PCX decoding +* :cve:`2020-10379` Fix two buffer overflows in TIFF decoding +* :cve:`2020-10994` Fix bounds overflow in JPEG 2000 decoding +* :cve:`2020-11538` Fix buffer overflow in SGI-RLE decoding Other Changes ============= diff --git a/docs/releasenotes/8.0.1.rst b/docs/releasenotes/8.0.1.rst index 3584a5d72e9..f7a1cea65da 100644 --- a/docs/releasenotes/8.0.1.rst +++ b/docs/releasenotes/8.0.1.rst @@ -4,7 +4,7 @@ Security ======== -Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`CVE-2020-15999`: +Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`2020-15999`: - A heap buffer overflow has been found in the handling of embedded PNG bitmaps, introduced in FreeType version 2.6. diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 8ed1d9d85cc..69726e628b7 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -11,7 +11,7 @@ Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022 when FreeType 2.8 will be the minimum supported. We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ @@ -40,13 +40,13 @@ This release includes security fixes. * An out-of-bounds read when saving TIFFs with custom metadata through LibTIFF * An out-of-bounds read when saving a GIF of 1px width -* :cve:`CVE-2020-35653` Buffer read overrun in PCX decoding +* :cve:`2020-35653` Buffer read overrun in PCX decoding The PCX image decoder used the reported image stride to calculate the row buffer, rather than calculating it from the image size. This issue dates back to the PIL fork. Thanks to Google's `OSS-Fuzz`_ project for finding this. -* :cve:`CVE-2020-35654` Fix TIFF out-of-bounds write error +* :cve:`2020-35654` Fix TIFF out-of-bounds write error Out-of-bounds write in ``TiffDecode.c`` when reading corrupt YCbCr files in some LibTIFF versions (4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). In some cases @@ -55,7 +55,7 @@ an out-of-bounds write in ``TiffDecode.c``. This potentially affects Pillow vers from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through `Tidelift`_. -* :cve:`CVE-2020-35655` Fix for SGI Decode buffer overrun +* :cve:`2020-35655` Fix for SGI Decode buffer overrun 4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly checking the offsets and length tables. Independently reported through `Tidelift`_ and Google's diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst index 4081c49ca5c..18d0a33f1cd 100644 --- a/docs/releasenotes/8.1.1.rst +++ b/docs/releasenotes/8.1.1.rst @@ -4,19 +4,19 @@ Security ======== -:cve:`CVE-2021-25289`: The previous fix for :cve:`CVE-2020-35654` was insufficient +:cve:`2021-25289`: The previous fix for :cve:`2020-35654` was insufficient due to incorrect error checking in ``TiffDecode.c``. -:cve:`CVE-2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` +:cve:`2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` with an invalid size. -:cve:`CVE-2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to +:cve:`2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to an out-of-bounds read in ``TIFFReadRGBATile``. -:cve:`CVE-2021-25292`: The PDF parser has a catastrophic backtracking regex +:cve:`2021-25292`: The PDF parser has a catastrophic backtracking regex that could be used as a DOS attack. -:cve:`CVE-2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, +:cve:`2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, since Pillow 4.3.0. diff --git a/docs/releasenotes/8.1.2.rst b/docs/releasenotes/8.1.2.rst index 50d132f3337..de50a3f1dad 100644 --- a/docs/releasenotes/8.1.2.rst +++ b/docs/releasenotes/8.1.2.rst @@ -4,8 +4,8 @@ Security ======== -There is an exhaustion of memory DOS in the BLP (:cve:`CVE-2021-27921`), -ICNS (:cve:`CVE-2021-27922`) and ICO (:cve:`CVE-2021-27923`) container formats +There is an exhaustion of memory DOS in the BLP (:cve:`2021-27921`), +ICNS (:cve:`2021-27922`) and ICO (:cve:`2021-27923`) container formats where Pillow did not properly check the reported size of the contained image. These images could cause arbitrarily large memory allocations. This was reported by Jiayi Lin, Luke Shaffer, Xinran Xie, and Akshay Ajayan of diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index f11953168a0..452077f1a64 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -129,15 +129,15 @@ Security These were all found with `OSS-Fuzz`_. -:cve:`CVE-2021-25287`, :cve:`CVE-2021-25288`: Fix OOB read in Jpeg2KDecode -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-25287`, :cve:`2021-25288`: Fix OOB read in Jpeg2KDecode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * For J2k images with multiple bands, it's legal to have different widths for each band, e.g. 1 byte for ``L``, 4 bytes for ``A``. * This dates to Pillow 2.4.0. -:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28675`: Fix DOS in PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input layers with regard to the size of the data block, this could lead to a @@ -145,15 +145,15 @@ These were all found with `OSS-Fuzz`_. :py:meth:`~PIL.Image.Image.load`. * This dates to the PIL fork. -:cve:`CVE-2021-28676`: Fix FLI DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28676`: Fix FLI DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``FliDecode.c`` did not properly check that the block advance was non-zero, potentially leading to an infinite loop on load. * This dates to the PIL fork. -:cve:`CVE-2021-28677`: Fix EPS DOS on _open -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28677`: Fix EPS DOS on _open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line endings. It accidentally used a quadratic method of accumulating lines while looking @@ -162,8 +162,8 @@ These were all found with `OSS-Fuzz`_. open phase, before an image was accepted for opening. * This dates to the PIL fork. -:cve:`CVE-2021-28678`: Fix BLP DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28678`: Fix BLP DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets returned data. This could lead to a denial-of-service where the decoder could be run a diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index 0bfead14470..b9642576f96 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -85,7 +85,7 @@ Security Buffer overflow ^^^^^^^^^^^^^^^ -This release addresses :cve:`CVE-2021-34552`. PIL since 1.1.4 and Pillow since 1.0 +This release addresses :cve:`2021-34552`. PIL since 1.1.4 and Pillow since 1.0 allowed parameters passed into a convert function to trigger buffer overflow in Convert.c. diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst index 6b5c759fc0a..3333d63a1e8 100644 --- a/docs/releasenotes/8.3.2.rst +++ b/docs/releasenotes/8.3.2.rst @@ -4,7 +4,7 @@ Security ======== -* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service) +* :cve:`2021-23437`: Avoid a potential ReDoS (regular expression denial of service) in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising :py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0. diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index 616cf4aa3e2..73e77ad3ef6 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -43,7 +43,7 @@ FreeType 2.7 Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _FreeType: https://freetype.org/ @@ -119,7 +119,7 @@ Google's `OSS-Fuzz`_ project for finding this issue. Restrict builtins available to ImageMath.eval ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:cve:`CVE-2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow +:cve:`2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will help prevent problems arising if users evaluate arbitrary expressions, such as ``ImageMath.eval("exec(exit())")``. @@ -127,7 +127,7 @@ help prevent problems arising if users evaluate arbitrary expressions, such as Fixed ImagePath.Path array handling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:cve:`CVE-2022-22815` (:cwe:`CWE-126`) and :cve:`CVE-2022-22816` (:cwe:`CWE-665`) were +:cve:`2022-22815` (:cwe:`126`) and :cve:`2022-22816` (:cwe:`665`) were found when initializing ``ImagePath.Path``. .. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index c1feee088b6..acb92dc4151 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -6,12 +6,12 @@ Security This release addresses several security problems. -:cve:`CVE-2022-24303`: If the path to the temporary directory on Linux or macOS +:cve:`2022-24303`: If the path to the temporary directory on Linux or macOS contained a space, this would break removal of the temporary image file after ``im.show()`` (and related actions), and potentially remove an unrelated file. This has been present since PIL. -:cve:`CVE-2022-22817`: While Pillow 9.0 restricted top-level builtins available to +:cve:`2022-22817`: While Pillow 9.0 restricted top-level builtins available to :py:meth:`PIL.ImageMath.eval`, it did not prevent builtins available to lambda expressions. These are now also restricted. diff --git a/docs/releasenotes/9.1.1.rst b/docs/releasenotes/9.1.1.rst index f8b155f3d6a..bab70f8f984 100644 --- a/docs/releasenotes/9.1.1.rst +++ b/docs/releasenotes/9.1.1.rst @@ -6,7 +6,7 @@ Security This release addresses several security problems. -:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines, +:cve:`2022-30595`: When reading a TGA file with RLE packets that cross scan lines, Pillow reads the information past the end of the first line without deducting that from the length of the remaining file data. This vulnerability was introduced in Pillow 9.1.0, and can cause a heap buffer overflow. diff --git a/setup.cfg b/setup.cfg index 824cae08845..d6057f1599d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,6 @@ docs = sphinx>=2.4 sphinx-copybutton sphinx-inline-tabs - sphinx-issues>=3.0.1 sphinx-removed-in sphinxext-opengraph tests = From 7703042301af8b5944fb708410b92fc98c042bdc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Mar 2023 18:20:43 +0200 Subject: [PATCH 406/727] Add missing html target to top-level Makefile (it's in the help) --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a2545b54e61..206b1cebeb2 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,8 @@ coverage: python3 -m coverage report .PHONY: doc -doc: +.PHONY: html +doc html: python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html From 542d25cceef7a706c6ee694ed574c3831de27b9d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Mar 2023 18:22:33 +0200 Subject: [PATCH 407/727] Add 'make htmlview' to open index page --- Makefile | 6 ++++++ docs/Makefile | 5 +++++ docs/make.bat | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/Makefile b/Makefile index 206b1cebeb2..1691e4657d5 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,11 @@ doc html: python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html +.PHONY: htmlview +htmlview: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs htmlview + .PHONY: doccheck doccheck: $(MAKE) doc @@ -40,6 +45,7 @@ help: @echo " doc make HTML docs" @echo " docserve run an HTTP server on the docs directory" @echo " html to make standalone HTML files" + @echo " htmlview to open the index page built by the html target in your browser" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" diff --git a/docs/Makefile b/docs/Makefile index 4a3de67cb26..3b4deb9bf9d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -19,6 +19,7 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" + @echo " htmlview to open the index page built by the html target in your browser" @echo " serve to start a local server for viewing docs" @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @@ -196,6 +197,10 @@ doctest: @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." +.PHONY: htmlview +htmlview: html + $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" + .PHONY: livehtml livehtml: html livereload $(BUILDDIR)/html -p 33233 diff --git a/docs/make.bat b/docs/make.bat index c943319addc..ad720cdcd18 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -19,6 +19,7 @@ if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files + echo. htmlview to open the index page built by the html target in your browser echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files @@ -52,6 +53,17 @@ if "%1" == "html" ( goto end ) +if "%1" == "htmlview" ( + cmd /C %this% html + + if EXIST "%BUILDDIR%\html\index.html" ( + echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... + start "" "%BUILDDIR%\html\index.html" + ) + + goto end +) + if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 From f046df2aba896bdc5b2c17a7fd892ef7ea5eff13 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 15 Mar 2023 14:17:42 +0200 Subject: [PATCH 408/727] Add colour to CI for readability --- .github/workflows/docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8a326547684..81ba8ef1506 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,6 +18,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: From 4d77a579ba03f1eb1e8166b5761f515b67a5c9da Mon Sep 17 00:00:00 2001 From: "Cimon Lucas (LCM)" Date: Wed, 15 Mar 2023 19:24:33 +0100 Subject: [PATCH 409/727] [Doc] Minor improvement made to c_extension_debugging.rst --- docs/reference/c_extension_debugging.rst | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index dc4c2bf94d0..7f29d2f2495 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -10,19 +10,13 @@ distributions. - ``python3-dbg`` package for the gdb extensions and python symbols - ``gdb`` and ``valgrind`` -- Potentially debug symbols for libraries. On ubuntu they're shipped - in package-dbgsym packages, from a different repo. +- Potentially debug symbols for libraries. On Ubuntu you can follow those + instructions to install the corresponding packages: `Debug Symbol Packages `_ -:: - - deb http://ddebs.ubuntu.com focal main restricted universe multiverse - deb http://ddebs.ubuntu.com focal-updates main restricted universe multiverse - deb http://ddebs.ubuntu.com focal-proposed main restricted universe multiverse - -Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym`` +Then ``sudo apt-get install libtiff5-dbgsym`` -- There's a bug with the dbg package for at least python 3.8 on ubuntu - 20.04, and you need to add a new link or two to make it autoload when +- There's a bug with the ``python3-dbg`` package for at least python 3.8 on + Ubuntu 20.04, and you need to add a new link or two to make it autoload when running python: :: From 1bded8335766604d50edf19bd37d068a5cbb3364 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Thu, 16 Mar 2023 07:52:19 +0100 Subject: [PATCH 410/727] Update docs/reference/c_extension_debugging.rst Co-authored-by: Hugo van Kemenade --- docs/reference/c_extension_debugging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 7f29d2f2495..5e85869058c 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -15,7 +15,7 @@ distributions. Then ``sudo apt-get install libtiff5-dbgsym`` -- There's a bug with the ``python3-dbg`` package for at least python 3.8 on +- There's a bug with the ``python3-dbg`` package for at least Python 3.8 on Ubuntu 20.04, and you need to add a new link or two to make it autoload when running python: From 0966293a0df0e45a2d4924542e180e917098c2fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Mar 2023 20:05:56 +1100 Subject: [PATCH 411/727] Consider transparency when applying blend mask --- Tests/images/blend_transparency.png | Bin 0 -> 211 bytes Tests/test_file_apng.py | 6 ++++++ src/PIL/PngImagePlugin.py | 10 +++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 Tests/images/blend_transparency.png diff --git a/Tests/images/blend_transparency.png b/Tests/images/blend_transparency.png new file mode 100644 index 0000000000000000000000000000000000000000..cef0a16de1f2db2db74e87d8bed5c8ba32038f28 GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryY$ZW{!9e;y1H%P7;~F4~BQZI| z2S|eyF?{ZwTmYn`(ja0WA+Ql31_Us_U|?Kvy6GB_$K&bZ7*a7O`N#PK3=GVSj0`9L zO?nR$hv|mt1kq3c(rvsW17w##T8d)`kY)m!knrO?kio>rAZ@=c0i@K^)z4*}Q$iB} Do3|(? literal 0 HcmV?d00001 diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index b2bec598496..feca72aa6de 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -163,6 +163,12 @@ def test_apng_blend(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) +def test_apng_blend_transparency(): + with Image.open("Tests/images/blend_transparency.png") as im: + im.seek(1) + assert im.getpixel((0, 0)) == (255, 0, 0) + + def test_apng_chunk_order(): with Image.open("Tests/images/apng/fctl_actl.png") as im: im.seek(im.n_frames - 1) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 9078957dcbe..15a3c8291c4 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1003,9 +1003,13 @@ def load_end(self): else: if self._prev_im and self.blend_op == Blend.OP_OVER: updated = self._crop(self.im, self.dispose_extent) - self._prev_im.paste( - updated, self.dispose_extent, updated.convert("RGBA") - ) + if self.im.mode == "RGB" and "transparency" in self.info: + mask = updated.convert_transparent( + "RGBA", self.info["transparency"] + ) + else: + mask = updated.convert("RGBA") + self._prev_im.paste(updated, self.dispose_extent, mask) self.im = self._prev_im if self.pyaccess: self.pyaccess = None From 5080d3581694d1a73a38845d17630808dd3fea10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Mar 2023 11:27:58 +1100 Subject: [PATCH 412/727] Allow libtiff_support_custom_tags to be missing --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 8c0431492fa..3d4d0910abd 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1807,7 +1807,7 @@ def _save(im, fp, filename): # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if not Image.core.libtiff_support_custom_tags: + if not getattr(Image.core, "libtiff_support_custom_tags", False): continue if tag in ifd.tagtype: From 80edcd18d162793d1e4dc7a87952bdedcee2ee5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Mar 2023 20:11:35 +1100 Subject: [PATCH 413/727] Do not render text if image has zero width or height --- src/PIL/ImageFont.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 30f6694e6cd..b08f107907e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -767,18 +767,19 @@ def getmask2( offset = offset[0] - stroke_width, offset[1] - stroke_width Image._decompression_bomb_check(size) im = fill("RGBA" if mode == "RGBA" else "L", size, 0) - self.font.render( - text, - im.id, - mode, - direction, - features, - language, - stroke_width, - ink, - start[0], - start[1], - ) + if min(size): + self.font.render( + text, + im.id, + mode, + direction, + features, + language, + stroke_width, + ink, + start[0], + start[1], + ) return im, offset def font_variant( From 16d235f8bc5055a961bfcad1006218171f305510 Mon Sep 17 00:00:00 2001 From: Auto-5 <84275465+Auto-5@users.noreply.github.com> Date: Fri, 17 Mar 2023 18:08:19 +0000 Subject: [PATCH 414/727] Fix order of arguments in docstring --- docs/reference/ImageDraw.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9565ab149b8..43a5a2bc2b3 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -320,8 +320,8 @@ Methods :param xy: Two points to define the bounding box. Sequence of either ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= y0``. The bounding box is inclusive of both endpoints. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. .. versionadded:: 5.3.0 @@ -334,8 +334,8 @@ Methods ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. :param corners: A tuple of whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. From c7d4d1f75a8cee77641629e5a9bf4408146188a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 18 Mar 2023 22:47:04 +1100 Subject: [PATCH 415/727] Fixed typo Co-authored-by: Aarni Koskela --- src/_imagingtk.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingtk.c b/src/_imagingtk.c index ac6a2313872..efa7fc1b6fa 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -60,7 +60,7 @@ PyInit__imagingtk(void) { m = PyModule_Create(&module_def); if (load_tkinter_funcs() != 0) { Py_DECREF(m); - return NULL;; + return NULL; } return m; } From f03f9670fa986bba4c765440c129220bd5f1e006 Mon Sep 17 00:00:00 2001 From: Auto-5 <84275465+Auto-5@users.noreply.github.com> Date: Sat, 18 Mar 2023 14:12:53 +0000 Subject: [PATCH 416/727] Removed incorrect docstring --- src/PIL/ImageDraw.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 5a0df09cb7e..4513d514fde 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -423,7 +423,6 @@ def draw_corners(pieslice): self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): - """Draw text.""" split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text From fd3da53b91f5d4d1dbf552b614a05fde789c3ed0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Mar 2023 06:46:22 +1100 Subject: [PATCH 417/727] Updated xz to 5.4.2 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2820bdb36cf..f363b9454dc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ def cmd_msbuild( "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.1.tar.gz/download", - "filename": "xz-5.4.1.tar.gz", - "dir": "xz-5.4.1", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download", + "filename": "xz-5.4.2.tar.gz", + "dir": "xz-5.4.2", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From fec92ce9cc214c12b66881e4da451993375903a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 19 Mar 2023 13:36:43 +1100 Subject: [PATCH 418/727] Restored deleted docstring --- src/PIL/ImageDraw.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 4513d514fde..8adcc87de51 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -463,6 +463,7 @@ def text( *args, **kwargs, ): + """Draw text.""" if self._multiline_check(text): return self.multiline_text( xy, From c3a7422ad3c579c98f87d5dcd5d073fd6dda618e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Mar 2023 22:36:37 +1100 Subject: [PATCH 419/727] Added Amazon Linux 2023 docker image --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index ff1605ac783..14592ea1d53 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -33,6 +33,7 @@ jobs: # Then run the remainder alpine, amazon-2-amd64, + amazon-2023-amd64, arch, centos-7-amd64, centos-stream-8-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index 6164a638bc5..cb2e4a74ad0 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -424,6 +424,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2023 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Arch | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS 7 | 3.9 | x86-64 | From 01cdfb6b27ed857fc63e625514a97352c5dc89cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Mar 2023 23:20:54 +1100 Subject: [PATCH 420/727] Removed duplicate calls to PyTuple_GET_SIZE --- src/_imaging.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 1c25ab00c1d..e7e403c9525 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -491,7 +491,7 @@ getink(PyObject *color, Imaging im, char *ink) { int g = 0, b = 0, a = 0; double f = 0; /* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a - python long (not int) that raises an overflow error when trying + Python long (not int) that raises an overflow error when trying to return it into a 32 bit C long */ PY_LONG_LONG r = 0; @@ -502,8 +502,12 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) { - color = PyTuple_GetItem(color, 0); + int tupleSize; + if (PyTuple_Check(color)) { + tupleSize = PyTuple_GET_SIZE(color); + if (tupleSize == 1) { + color = PyTuple_GetItem(color, 0); + } } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || im->type == IMAGING_TYPE_SPECIAL) { @@ -531,7 +535,7 @@ getink(PyObject *color, Imaging im, char *ink) { if (im->bands == 1) { /* unsigned integer, single layer */ if (rIsInt != 1) { - if (PyTuple_GET_SIZE(color) != 1) { + if (tupleSize != 1) { PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); return NULL; } else if (!PyArg_ParseTuple(color, "L", &r)) { @@ -541,7 +545,6 @@ getink(PyObject *color, Imaging im, char *ink) { ink[0] = (char)CLIP8(r); ink[1] = ink[2] = ink[3] = 0; } else { - a = 255; if (rIsInt) { /* compatibility: ABGR */ a = (UINT8)(r >> 24); @@ -549,7 +552,7 @@ getink(PyObject *color, Imaging im, char *ink) { g = (UINT8)(r >> 8); r = (UINT8)r; } else { - int tupleSize = PyTuple_GET_SIZE(color); + a = 255; if (im->bands == 2) { if (tupleSize != 1 && tupleSize != 2) { PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements"); From 11d100ce5deb6028ca91633e75a01d648dc936c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Mar 2023 00:30:10 +1100 Subject: [PATCH 421/727] Support creating BGR;15, BGR;16 and BGR;24 images --- Tests/test_image.py | 7 +++--- Tests/test_image_access.py | 14 ++++++------ src/PIL/ImageMode.py | 6 ++--- src/_imaging.c | 45 +++++++++++++++++++++++++++++++++----- src/libImaging/Storage.c | 6 ++--- 5 files changed, 56 insertions(+), 22 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index c226285099e..17f1edb00d1 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -48,6 +48,9 @@ class TestImage: "RGBX", "RGBA", "RGBa", + "BGR;15", + "BGR;16", + "BGR;24", "CMYK", "YCbCr", "LAB", @@ -57,9 +60,7 @@ class TestImage: def test_image_modes_success(self, mode): Image.new(mode, (1, 1)) - @pytest.mark.parametrize( - "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24") - ) + @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode): with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4079d935800..29eed745e90 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -363,8 +363,8 @@ def test_p_putpixel_rgb_rgba(self, mode): class TestImagePutPixelError(AccessTest): - IMAGE_MODES1 = ["L", "LA", "RGB", "RGBA"] - IMAGE_MODES2 = ["I", "I;16", "BGR;15"] + IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] + IMAGE_MODES2 = ["L", "I", "I;16"] INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) @@ -379,6 +379,11 @@ def test_putpixel_type_error1(self, mode): ( ("L", (0, 2), "color must be int or single-element tuple"), ("LA", (0, 3), "color must be int, or tuple of one or two elements"), + ( + "BGR;15", + (0, 2), + "color must be int, or tuple of one or three elements", + ), ( "RGB", (0, 2, 5), @@ -407,11 +412,6 @@ def test_putpixel_overflow_error(self, mode): with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) - def test_putpixel_unrecognized_mode(self): - im = hopper("BGR;15") - with pytest.raises(ValueError, match="unrecognized image mode"): - im.putpixel((0, 0), 0) - class TestEmbeddable: @pytest.mark.xfail(reason="failing test") diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 8b1506e9bcc..a0b33514296 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -58,9 +58,9 @@ def getmode(mode): "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), # extra experimental modes "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), diff --git a/src/_imaging.c b/src/_imaging.c index e7e403c9525..3b17d638e8a 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -517,15 +517,13 @@ getink(PyObject *color, Imaging im, char *ink) { return NULL; } rIsInt = 1; - } else if (im->type == IMAGING_TYPE_UINT8) { - if (!PyTuple_Check(color)) { - PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); - return NULL; - } - } else { + } else if (im->bands == 1) { PyErr_SetString( PyExc_TypeError, "color must be int or single-element tuple"); return NULL; + } else if (!PyTuple_Check(color)) { + PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); + return NULL; } } @@ -596,6 +594,41 @@ getink(PyObject *color, Imaging im, char *ink) { ink[1] = (UINT8)(r >> 8); ink[2] = ink[3] = 0; return ink; + } else { + if (rIsInt) { + b = (UINT8)(r >> 16); + g = (UINT8)(r >> 8); + r = (UINT8)r; + } else if (tupleSize != 3) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "Lii", &r, &g, &b)) { + return NULL; + } + if (!strcmp(im->mode, "BGR;15")) { + UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + + ((((UINT16)g) << 2) & 0x03e0) + + ((((UINT16)b) >> 3) & 0x001f); + + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;16")) { + UINT16 v = ((((UINT16)r) << 8) & 0xf800) + + ((((UINT16)g) << 3) & 0x07e0) + + ((((UINT16)b) >> 3) & 0x001f); + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;24")) { + ink[0] = (UINT8)b; + ink[1] = (UINT8)g; + ink[2] = (UINT8)r; + ink[3] = 0; + return ink; + } } } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7730b4be823..7cf00ef3558 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -131,7 +131,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ /* 15-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; @@ -139,7 +139,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;16") == 0) { /* EXPERIMENTAL */ /* 16-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; @@ -147,7 +147,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;24") == 0) { /* EXPERIMENTAL */ /* 24-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 3; im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; From 63286622488ffaeb481f74a0c499fd8732df98e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Mar 2023 17:34:34 +1100 Subject: [PATCH 422/727] PyUnicode_* may return NULL --- src/_imaging.c | 14 +++++++------- src/_imagingcms.c | 2 +- src/_imagingft.c | 4 ++-- src/_imagingmorph.c | 2 +- src/_webp.c | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 847eed5cebe..96bfe5df730 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4213,7 +4213,7 @@ setup_module(PyObject *m) { extern const char *ImagingJpegVersion(void); PyObject *jpeglib_version = PyUnicode_FromString(ImagingJpegVersion()); PyDict_SetItemString(d, "jpeglib_version", jpeglib_version); - Py_DECREF(jpeglib_version); + Py_XDECREF(jpeglib_version); } #endif @@ -4222,7 +4222,7 @@ setup_module(PyObject *m) { extern const char *ImagingJpeg2KVersion(void); PyObject *jp2klib_version = PyUnicode_FromString(ImagingJpeg2KVersion()); PyDict_SetItemString(d, "jp2klib_version", jp2klib_version); - Py_DECREF(jp2klib_version); + Py_XDECREF(jp2klib_version); } #endif @@ -4233,7 +4233,7 @@ setup_module(PyObject *m) { #define tostr(a) tostr1(a) PyObject *libjpeg_turbo_version = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); PyDict_SetItemString(d, "libjpeg_turbo_version", libjpeg_turbo_version); - Py_DECREF(libjpeg_turbo_version); + Py_XDECREF(libjpeg_turbo_version); #undef tostr #undef tostr1 #else @@ -4249,7 +4249,7 @@ setup_module(PyObject *m) { extern const char *ImagingImageQuantVersion(void); PyObject *imagequant_version = PyUnicode_FromString(ImagingImageQuantVersion()); PyDict_SetItemString(d, "imagequant_version", imagequant_version); - Py_DECREF(imagequant_version); + Py_XDECREF(imagequant_version); } #else have_libimagequant = Py_False; @@ -4268,7 +4268,7 @@ setup_module(PyObject *m) { extern const char *ImagingZipVersion(void); PyObject *zlibversion = PyUnicode_FromString(ImagingZipVersion()); PyDict_SetItemString(d, "zlib_version", zlibversion); - Py_DECREF(zlibversion); + Py_XDECREF(zlibversion); } #endif @@ -4277,7 +4277,7 @@ setup_module(PyObject *m) { extern const char *ImagingTiffVersion(void); PyObject *libtiff_version = PyUnicode_FromString(ImagingTiffVersion()); PyDict_SetItemString(d, "libtiff_version", libtiff_version); - Py_DECREF(libtiff_version); + Py_XDECREF(libtiff_version); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4302,7 +4302,7 @@ setup_module(PyObject *m) { PyObject *pillow_version = PyUnicode_FromString(version); PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version); - Py_DECREF(pillow_version); + Py_XDECREF(pillow_version); return 0; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 779f31b9cf6..521151ae017 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1534,7 +1534,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } PyDict_SetItemString(d, "littlecms_version", v); - Py_DECREF(v); + Py_XDECREF(v); return 0; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 8697a74ff53..beab7e0bd9b 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1366,7 +1366,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); PyDict_SetItemString(d, "freetype2_version", v); - Py_DECREF(v); + Py_XDECREF(v); #ifdef HAVE_RAQM #if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) @@ -1392,7 +1392,7 @@ setup_module(PyObject *m) { v = Py_None; #endif PyDict_SetItemString(d, "raqm_version", v); - Py_DECREF(v); + Py_XDECREF(v); #ifdef FRIBIDI_MAJOR_VERSION { diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 46a40e96daf..7f85bc096d2 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -244,7 +244,7 @@ setup_module(PyObject *m) { PyObject *version = PyUnicode_FromString("0.1"); PyDict_SetItemString(d, "__version", version); - Py_DECREF(version); + Py_XDECREF(version); return 0; } diff --git a/src/_webp.c b/src/_webp.c index 5575e04f9ce..5c86341faaa 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -964,7 +964,7 @@ setup_module(PyObject *m) { PyObject *webpdecoder_version = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", webpdecoder_version); - Py_DECREF(webpdecoder_version); + Py_XDECREF(webpdecoder_version); #ifdef HAVE_WEBPANIM /* Ready object types */ From dfeed0eb7ea1f6a89921f9a2392c111740884ce3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Mar 2023 22:44:14 +1100 Subject: [PATCH 423/727] Group decrementing reference counts for previous axes in font_getvaraxes --- src/_imagingft.c | 49 +++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 93866ec4da0..bf9a242877c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1114,7 +1114,7 @@ font_getvarnames(FontObject *self) { static PyObject * font_getvaraxes(FontObject *self) { - int error; + int error, failed = 0; FT_UInt i, j, num_axis, name_count; FT_MM_Var *master; FT_Var_Axis axis; @@ -1137,36 +1137,39 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); if (list_axis == NULL) { + failed = 1; + } else { + PyDict_SetItemString( + list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); + PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); + PyDict_SetItemString( + list_axis, "maximum", PyLong_FromLong(axis.maximum / 65536)); + + for (j = 0; j < name_count; j++) { + error = FT_Get_Sfnt_Name(self->face, j, &name); + if (error) { + Py_DECREF(list_axis); + failed = 1; + break; + } + + if (name.name_id == axis.strid) { + axis_name = Py_BuildValue("y#", name.string, name.string_len); + PyDict_SetItemString(list_axis, "name", axis_name); + break; + } + } + } + if (failed) { for (j = 0; j < i; j++) { list_axis = PyList_GetItem(list_axes, j); Py_DECREF(list_axis); } Py_DECREF(list_axes); - return NULL; - } - PyDict_SetItemString( - list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); - PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); - PyDict_SetItemString( - list_axis, "maximum", PyLong_FromLong(axis.maximum / 65536)); - - for (j = 0; j < name_count; j++) { - error = FT_Get_Sfnt_Name(self->face, j, &name); if (error) { - Py_DECREF(list_axis); - for (j = 0; j < i; j++) { - list_axis = PyList_GetItem(list_axes, j); - Py_DECREF(list_axis); - } - Py_DECREF(list_axes); return geterror(error); } - - if (name.name_id == axis.strid) { - axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name); - break; - } + return NULL; } PyList_SetItem(list_axes, i, list_axis); From 76d36da12e8b17d7e91814056c88bea5b0c614fa Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Mar 2023 00:59:00 +0000 Subject: [PATCH 424/727] avoid Py_DECREF(Py_None) --- src/_imagingft.c | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index beab7e0bd9b..dfb3697e2ba 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1365,7 +1365,7 @@ setup_module(PyObject *m) { FT_Library_Version(library, &major, &minor, &patch); v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); - PyDict_SetItemString(d, "freetype2_version", v); + PyDict_SetItemString(d, "freetype2_version", v ? v : Py_None); Py_XDECREF(v); #ifdef HAVE_RAQM @@ -1386,35 +1386,32 @@ setup_module(PyObject *m) { PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); Py_DECREF(v); if (have_raqm) { + v = NULL; #ifdef RAQM_VERSION_MAJOR v = PyUnicode_FromString(raqm_version_string()); -#else - v = Py_None; #endif - PyDict_SetItemString(d, "raqm_version", v); + PyDict_SetItemString(d, "raqm_version", v ? v : Py_None); Py_XDECREF(v); + v = NULL; #ifdef FRIBIDI_MAJOR_VERSION { const char *a = strchr(fribidi_version_info, ')'); const char *b = strchr(fribidi_version_info, '\n'); if (a && b && a + 2 < b) { v = PyUnicode_FromStringAndSize(a + 2, b - (a + 2)); - } else { - v = Py_None; } } -#else - v = Py_None; #endif - PyDict_SetItemString(d, "fribidi_version", v); + PyDict_SetItemString(d, "fribidi_version", v ? v : Py_None); + Py_XDECREF(v); + v = NULL; #ifdef HB_VERSION_STRING v = PyUnicode_FromString(hb_version_string()); -#else - v = Py_None; #endif - PyDict_SetItemString(d, "harfbuzz_version", v); + PyDict_SetItemString(d, "harfbuzz_version", v ? v : Py_None); + Py_XDECREF(v); } return 0; From b3bf1ca6d9ca1d8f6ab3118448cfc4e27984e0f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Mar 2023 22:36:36 +1100 Subject: [PATCH 425/727] Fixed calling html target from htmlview --- docs/make.bat | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/make.bat b/docs/make.bat index ad720cdcd18..0ed5ee1a57e 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -45,20 +45,20 @@ if "%1" == "clean" ( goto end ) -if "%1" == "html" ( +set html=false +if "%1%" == "html" set html=true +if "%1%" == "htmlview" set html=true +if "%html%" == "true" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "htmlview" ( - cmd /C %this% html - if EXIST "%BUILDDIR%\html\index.html" ( - echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... - start "" "%BUILDDIR%\html\index.html" + if "%1" == "htmlview" ( + if EXIST "%BUILDDIR%\html\index.html" ( + echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... + start "" "%BUILDDIR%\html\index.html" + ) ) goto end From 3a262f0523cb2aea44a1678a282ad2d0ec9014c3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 21 Mar 2023 14:05:58 +0200 Subject: [PATCH 426/727] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Makefile | 4 ++-- docs/conf.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1691e4657d5..bb0ea60b35a 100644 --- a/Makefile +++ b/Makefile @@ -44,8 +44,8 @@ help: @echo " coverage run coverage test (in progress)" @echo " doc make HTML docs" @echo " docserve run an HTTP server on the docs directory" - @echo " html to make standalone HTML files" - @echo " htmlview to open the index page built by the html target in your browser" + @echo " html make HTML docs" + @echo " htmlview open the index page built by the html target in your browser" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" diff --git a/docs/conf.py b/docs/conf.py index 683ff7856cb..2ebcd6b2e10 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,8 +28,8 @@ # ones. extensions = [ "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_inline_tabs", From 1a11ba662c244bbb6ab3b7c3753eea8887d3c99f Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Mar 2023 21:44:52 +0000 Subject: [PATCH 427/727] do not insert null into dict --- src/_imaging.c | 77 ++++++++++++++++++++++++----------------------- src/_imagingcms.c | 2 +- src/_imagingft.c | 8 ++--- src/_webp.c | 6 ++-- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 96bfe5df730..2229235dba2 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3810,6 +3810,7 @@ static PyTypeObject PixelAccess_Type = { static PyObject * _get_stats(PyObject *self, PyObject *args) { PyObject *d; + PyObject *v; ImagingMemoryArena arena = &ImagingDefaultArena; if (!PyArg_ParseTuple(args, ":get_stats")) { @@ -3820,29 +3821,29 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } - PyObject *new_count = PyLong_FromLong(arena->stats_new_count); - PyDict_SetItemString(d, "new_count", new_count); - Py_XDECREF(new_count); + v = PyLong_FromLong(arena->stats_new_count); + PyDict_SetItemString(d, "new_count", v ? v : Py_None); + Py_XDECREF(v); - PyObject *allocated_blocks = PyLong_FromLong(arena->stats_allocated_blocks); - PyDict_SetItemString(d, "allocated_blocks", allocated_blocks); - Py_XDECREF(allocated_blocks); + v = PyLong_FromLong(arena->stats_allocated_blocks); + PyDict_SetItemString(d, "allocated_blocks", v ? v : Py_None); + Py_XDECREF(v); - PyObject *reused_blocks = PyLong_FromLong(arena->stats_reused_blocks); - PyDict_SetItemString(d, "reused_blocks", reused_blocks); - Py_XDECREF(reused_blocks); + v = PyLong_FromLong(arena->stats_reused_blocks); + PyDict_SetItemString(d, "reused_blocks", v ? v : Py_None); + Py_XDECREF(v); - PyObject *reallocated_blocks = PyLong_FromLong(arena->stats_reallocated_blocks); - PyDict_SetItemString(d, "reallocated_blocks", reallocated_blocks); - Py_XDECREF(reallocated_blocks); + v = PyLong_FromLong(arena->stats_reallocated_blocks); + PyDict_SetItemString(d, "reallocated_blocks", v ? v : Py_None); + Py_XDECREF(v); - PyObject *freed_blocks = PyLong_FromLong(arena->stats_freed_blocks); - PyDict_SetItemString(d, "freed_blocks", freed_blocks); - Py_XDECREF(freed_blocks); + v = PyLong_FromLong(arena->stats_freed_blocks); + PyDict_SetItemString(d, "freed_blocks", v ? v : Py_None); + Py_XDECREF(v); - PyObject *blocks_cached = PyLong_FromLong(arena->blocks_cached); - PyDict_SetItemString(d, "blocks_cached", blocks_cached); - Py_XDECREF(blocks_cached); + v = PyLong_FromLong(arena->blocks_cached); + PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); + Py_XDECREF(v); return d; } @@ -4211,31 +4212,33 @@ setup_module(PyObject *m) { #ifdef HAVE_LIBJPEG { extern const char *ImagingJpegVersion(void); - PyObject *jpeglib_version = PyUnicode_FromString(ImagingJpegVersion()); - PyDict_SetItemString(d, "jpeglib_version", jpeglib_version); - Py_XDECREF(jpeglib_version); + PyObject *v = PyUnicode_FromString(ImagingJpegVersion()); + PyDict_SetItemString(d, "jpeglib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif #ifdef HAVE_OPENJPEG { extern const char *ImagingJpeg2KVersion(void); - PyObject *jp2klib_version = PyUnicode_FromString(ImagingJpeg2KVersion()); - PyDict_SetItemString(d, "jp2klib_version", jp2klib_version); - Py_XDECREF(jp2klib_version); + PyObject *v = PyUnicode_FromString(ImagingJpeg2KVersion()); + PyDict_SetItemString(d, "jp2klib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif PyObject *have_libjpegturbo; #ifdef LIBJPEG_TURBO_VERSION have_libjpegturbo = Py_True; + { #define tostr1(a) #a #define tostr(a) tostr1(a) - PyObject *libjpeg_turbo_version = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); - PyDict_SetItemString(d, "libjpeg_turbo_version", libjpeg_turbo_version); - Py_XDECREF(libjpeg_turbo_version); + PyObject *v = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); + PyDict_SetItemString(d, "libjpeg_turbo_version", v ? v : Py_None); + Py_XDECREF(v); #undef tostr #undef tostr1 + } #else have_libjpegturbo = Py_False; #endif @@ -4247,9 +4250,9 @@ setup_module(PyObject *m) { have_libimagequant = Py_True; { extern const char *ImagingImageQuantVersion(void); - PyObject *imagequant_version = PyUnicode_FromString(ImagingImageQuantVersion()); - PyDict_SetItemString(d, "imagequant_version", imagequant_version); - Py_XDECREF(imagequant_version); + PyObject *v = PyUnicode_FromString(ImagingImageQuantVersion()); + PyDict_SetItemString(d, "imagequant_version", v ? v : Py_None); + Py_XDECREF(v); } #else have_libimagequant = Py_False; @@ -4266,18 +4269,18 @@ setup_module(PyObject *m) { PyModule_AddIntConstant(m, "FIXED", Z_FIXED); { extern const char *ImagingZipVersion(void); - PyObject *zlibversion = PyUnicode_FromString(ImagingZipVersion()); - PyDict_SetItemString(d, "zlib_version", zlibversion); - Py_XDECREF(zlibversion); + PyObject *v = PyUnicode_FromString(ImagingZipVersion()); + PyDict_SetItemString(d, "zlib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); - PyObject *libtiff_version = PyUnicode_FromString(ImagingTiffVersion()); - PyDict_SetItemString(d, "libtiff_version", libtiff_version); - Py_XDECREF(libtiff_version); + PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); + PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); + Py_XDECREF(v); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4301,7 +4304,7 @@ setup_module(PyObject *m) { PyModule_AddObject(m, "HAVE_XCB", have_xcb); PyObject *pillow_version = PyUnicode_FromString(version); - PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version); + PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); Py_XDECREF(pillow_version); return 0; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 521151ae017..ddfe6ad6486 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1533,7 +1533,7 @@ setup_module(PyObject *m) { } else { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } - PyDict_SetItemString(d, "littlecms_version", v); + PyDict_SetItemString(d, "littlecms_version", v ? v : Py_None); Py_XDECREF(v); return 0; diff --git a/src/_imagingft.c b/src/_imagingft.c index dfb3697e2ba..4f44d6a7158 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1130,15 +1130,15 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); - PyDict_SetItemString(list_axis, "minimum", minimum); + PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None); Py_XDECREF(minimum); PyObject *def = PyLong_FromLong(axis.def / 65536); - PyDict_SetItemString(list_axis, "default", def); + PyDict_SetItemString(list_axis, "default", def ? def : Py_None); Py_XDECREF(def); PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); - PyDict_SetItemString(list_axis, "maximum", maximum); + PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); Py_XDECREF(maximum); for (j = 0; j < name_count; j++) { @@ -1149,7 +1149,7 @@ font_getvaraxes(FontObject *self) { if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name); + PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); Py_XDECREF(axis_name); break; } diff --git a/src/_webp.c b/src/_webp.c index 5c86341faaa..31055fecd0c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -962,9 +962,9 @@ setup_module(PyObject *m) { addAnimFlagToModule(m); addTransparencyFlagToModule(m); - PyObject *webpdecoder_version = PyUnicode_FromString(WebPDecoderVersion_str()); - PyDict_SetItemString(d, "webpdecoder_version", webpdecoder_version); - Py_XDECREF(webpdecoder_version); + PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); + PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); + Py_XDECREF(v); #ifdef HAVE_WEBPANIM /* Ready object types */ From e1d0a96404fb98ddbbcba5d7a04f1e836adccdf8 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Mar 2023 21:46:33 +0000 Subject: [PATCH 428/727] remove unused version value --- src/_imagingmorph.c | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 7f85bc096d2..2e3545863c7 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -238,17 +238,6 @@ get_on_pixels(PyObject *self, PyObject *args) { return ret; } -static int -setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - - PyObject *version = PyUnicode_FromString("0.1"); - PyDict_SetItemString(d, "__version", version); - Py_XDECREF(version); - - return 0; -} - static PyMethodDef functions[] = { /* Functions */ {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, @@ -270,9 +259,5 @@ PyInit__imagingmorph(void) { m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - return NULL; - } - return m; } From adae44da0dd5c6f7ce50701fecec39ae1a9ac88d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Mar 2023 09:10:47 +1100 Subject: [PATCH 429/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d7b5b4daba3..706d9ad76b6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow libtiff_support_custom_tags to be missing #7020 + [radarhere] + - Improved I;16N support #6834 [radarhere] From 415455d01b1808e7b65ba88908dc33d0c5172ce2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Mar 2023 09:05:09 +1100 Subject: [PATCH 430/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 706d9ad76b6..09462a2640d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added reading of JPEG2000 comments #6909 + [radarhere] + +- Decrement reference count #7003 + [radarhere, nulano] + - Allow libtiff_support_custom_tags to be missing #7020 [radarhere] From 6a931861fe45196754a0de2cf8f96c5821d62964 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Mar 2023 10:12:18 +1100 Subject: [PATCH 431/727] Load before getting size in __array_interface__ --- Tests/test_numpy.py | 9 ++++++++- src/PIL/Image.py | 7 ++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index a8bbcbdb8bc..147f94a71fd 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -4,7 +4,7 @@ from PIL import Image -from .helper import assert_deep_equal, assert_image, hopper +from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature numpy = pytest.importorskip("numpy", reason="NumPy not installed") @@ -219,6 +219,13 @@ def test_zero_size(): assert im.size == (0, 0) +@skip_unless_feature("libtiff") +def test_load_first(): + with Image.open("Tests/images/g4_orientation_5.tif") as im: + a = numpy.array(im) + assert a.shape == (88, 590) + + def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cf9ab2df6b1..95f5a9bc175 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -686,11 +686,7 @@ def _repr_png_(self): @property def __array_interface__(self): # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new["shape"] = shape - new["typestr"] = typestr - new["version"] = 3 + new = {"version": 3} try: if self.mode == "1": # Binary images need to be extended from bits to bytes @@ -709,6 +705,7 @@ def __array_interface__(self): if parse_version(numpy.__version__) < parse_version("1.23"): warnings.warn(e) raise + new["shape"], new["typestr"] = _conv_type_shape(self) return new def __getstate__(self): From d3923f71420c58f8545fc74fc5f41f0b415635f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Mar 2023 17:53:35 +1100 Subject: [PATCH 432/727] Use reading of comments to test saving comments --- Tests/test_file_jpeg2k.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 8261c612ab5..a869d74f0ce 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -372,6 +372,20 @@ def test_comment(): pass +def test_save_comment(): + comment = "Created by Pillow" + out = BytesIO() + test_card.save(out, "JPEG2000", comment=comment) + out.seek(0) + + with Image.open(out) as im: + assert im.info["comment"] == b"Created by Pillow" + + too_long_comment = " " * 65532 + with pytest.raises(ValueError): + test_card.save(out, "JPEG2000", comment=too_long_comment) + + @pytest.mark.parametrize( "test_file", [ @@ -391,20 +405,6 @@ def test_crashes(test_file): pass -def test_custom_comment(): - output_stream = BytesIO() - unique_comment = "This is a unique comment, which should be found below" - test_card.save(output_stream, "JPEG2000", comment=unique_comment) - output_stream.seek(0) - data = output_stream.read() - # Lazy method to determine if the comment is in the image generated - assert bytes(unique_comment, "utf-8") in data - - too_long_comment = " " * 65532 - with pytest.raises(ValueError): - test_card.save(output_stream, "JPEG2000", comment=too_long_comment) - - @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) From 7c3fd254330ebcbd51fa08ef0b709b52587f5b92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Mar 2023 09:45:51 +1100 Subject: [PATCH 433/727] Allow saving bytes as comments --- Tests/test_file_jpeg2k.py | 14 +++++++------- src/PIL/Jpeg2KImagePlugin.py | 4 +++- src/encode.c | 21 +++++++++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a869d74f0ce..60be50e0747 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -373,15 +373,15 @@ def test_comment(): def test_save_comment(): - comment = "Created by Pillow" - out = BytesIO() - test_card.save(out, "JPEG2000", comment=comment) - out.seek(0) + for comment in ("Created by Pillow", b"Created by Pillow"): + out = BytesIO() + test_card.save(out, "JPEG2000", comment=comment) + out.seek(0) - with Image.open(out) as im: - assert im.info["comment"] == b"Created by Pillow" + with Image.open(out) as im: + assert im.info["comment"] == b"Created by Pillow" - too_long_comment = " " * 65532 + too_long_comment = " " * 65531 with pytest.raises(ValueError): test_card.save(out, "JPEG2000", comment=too_long_comment) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 4249fe7141c..980c299dbce 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -351,10 +351,12 @@ def _save(im, fp, filename): cinema_mode = info.get("cinema_mode", "no") mct = info.get("mct", 0) signed = info.get("signed", False) - fd = -1 comment = info.get("comment") + if isinstance(comment, str): + comment = comment.encode() add_plt = info.get("add_plt", False) + fd = -1 if hasattr(fp, "fileno"): try: fd = fp.fileno() diff --git a/src/encode.c b/src/encode.c index e8946dbaef1..7dcb7976632 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1215,11 +1215,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { int sgnd = 0; Py_ssize_t fd = -1; char *comment = NULL; + Py_ssize_t comment_size; int add_plt = 0; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbnzp", + "ss|OOOsOnOOOssbbnz#p", &mode, &format, &offset, @@ -1237,6 +1238,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &sgnd, &fd, &comment, + &comment_size, &add_plt)) { return NULL; } @@ -1319,9 +1321,9 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } } - if (comment != NULL && strlen(comment) > 0) { + if (comment && comment_size > 0) { /* Size is stored as as an uint16, subtract 4 bytes for the header */ - if (strlen(comment) >= 65531) { + if (comment_size >= 65531) { PyErr_SetString( PyExc_ValueError, "JPEG 2000 comment is too long"); @@ -1329,15 +1331,14 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { return NULL; } - context->comment = strdup(comment); - - if (context->comment == NULL) { - PyErr_SetString( - PyExc_MemoryError, - "Couldn't allocate memory for JPEG 2000 comment"); + char *p = malloc(comment_size + 1); + if (!p) { Py_DECREF(encoder); - return NULL; + return ImagingError_MemoryError(); } + memcpy(p, comment, comment_size); + p[comment_size] = '\0'; + context->comment = p; } if (quality_layers && PySequence_Check(quality_layers)) { From 311ab716e055cbb90cf5415138893d0bab99a357 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Mar 2023 09:48:25 +1100 Subject: [PATCH 434/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 09462a2640d..6861c61a1da 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Consider transparency when applying APNG blend mask #7018 + [radarhere] + +- Round duration when saving animated WebP images #6996 + [radarhere] + - Added reading of JPEG2000 comments #6909 [radarhere] From ce1acb9a75bb96862db5fbdafbd9b8e129f71d71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Mar 2023 03:50:20 +0000 Subject: [PATCH 435/727] Update actions/stale action to v8 --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8c210bc9096..24b8f85d119 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v7 + uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" From 5e7c1801e0a31d26a10988449f208f9cf26c43f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Mar 2023 11:32:22 +1100 Subject: [PATCH 436/727] Install Ghostscript using Chocolatey --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index d4dd2dc953b..b5913e04386 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -27,8 +27,8 @@ install: - mv c:\pillow-depends-main c:\pillow-depends - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ -- ..\pillow-depends\gs1000w32.exe /S -- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% +- choco install ghostscript --version=10.0.0.20230317 +- path c:\nasm-2.15.05;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 833f096c310..109e8d0581f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -74,8 +74,8 @@ jobs: 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH - winbuild\depends\gs1000w32.exe /S - echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.0.0.20230317 + echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From a236c272a801b7d7feab591412745656b1b88fc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Mar 2023 23:41:27 +1100 Subject: [PATCH 437/727] Added release notes --- docs/releasenotes/9.5.0.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index bd6e586932a..849e87594de 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -63,3 +63,13 @@ Added support for saving PDFs in RGBA mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using the JPXDecode filter, PDFs can now be saved in RGBA mode. + +BGR;* modes +^^^^^^^^^^^ + +It is now possible to create new BGR;15, BGR;16 and BGR;24 images. Conversely, BGR;32 +has been removed from ImageMode and its associated methods, dropping the little support +Pillow had for the mode. + +With that, all modes listed under :ref:`concept-modes` can now be used to create a new +image. From bdcc6333b697f82ae12139385f2c02f8e4d8b0fd Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 26 Mar 2023 08:34:42 +1100 Subject: [PATCH 438/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6861c61a1da..bbe47473de8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Load before getting size in __array_interface__ #7034 + [radarhere] + +- Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 #7010 + [radarhere] + - Consider transparency when applying APNG blend mask #7018 [radarhere] From 1fd189164c8905d2f99f0d9836ef7ea4c897b055 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Mar 2023 22:38:29 +1100 Subject: [PATCH 439/727] Renamed "add_plt" to "plt" --- Tests/test_file_jpeg2k.py | 2 +- src/PIL/Jpeg2KImagePlugin.py | 4 ++-- src/encode.c | 6 +++--- src/libImaging/Jpeg2K.h | 2 +- src/libImaging/Jpeg2KEncode.c | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 60be50e0747..7b512695b92 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -409,7 +409,7 @@ def test_crashes(test_file): def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + test_card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: box_bytes = out.read(2) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 980c299dbce..e7d91c8186d 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -354,7 +354,7 @@ def _save(im, fp, filename): comment = info.get("comment") if isinstance(comment, str): comment = comment.encode() - add_plt = info.get("add_plt", False) + plt = info.get("plt", False) fd = -1 if hasattr(fp, "fileno"): @@ -379,7 +379,7 @@ def _save(im, fp, filename): signed, fd, comment, - add_plt, + plt, ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) diff --git a/src/encode.c b/src/encode.c index 7dcb7976632..8aa357b6ca4 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1216,7 +1216,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { Py_ssize_t fd = -1; char *comment = NULL; Py_ssize_t comment_size; - int add_plt = 0; + int plt = 0; if (!PyArg_ParseTuple( args, @@ -1239,7 +1239,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &fd, &comment, &comment_size, - &add_plt)) { + &plt)) { return NULL; } @@ -1358,7 +1358,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->cinema_mode = cine_mode; context->mct = mct; context->sgnd = sgnd; - context->add_plt = add_plt; + context->plt = plt; return (PyObject *)encoder; } diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index 7bf8b4b0a74..e8d92f7b6bc 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -101,7 +101,7 @@ typedef struct { char *comment; /* Include PLT marker segment */ - int add_plt; + int plt; } JPEG2KENCODESTATE; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 6d6add76ee9..a7c6441979b 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -502,7 +502,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ #if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) - if (context->add_plt) { + if (context->plt) { const char * plt_option[2] = {"PLT=YES", NULL}; opj_encoder_set_extra_options(codec, plt_option); } From a7df096d1b03d53c9834989e3e2e5e4fb8b37f4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Mar 2023 22:39:01 +1100 Subject: [PATCH 440/727] Added release notes --- docs/releasenotes/9.5.0.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 074931671b0..20d7ee893cf 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -48,11 +48,16 @@ Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` ``corners``. This a tuple of Booleans, specifying whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. -Reading JPEG comments -^^^^^^^^^^^^^^^^^^^^^ +JPEG2000 comments and PLT marker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When opening a JPEG2000 image, the comment may now be read into -:py:attr:`~PIL.Image.Image.info`. +:py:attr:`~PIL.Image.Image.info`. The ``comment`` keyword argument can be used +to save it back again. + +If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument +is present and true when saving JPEG2000 images, tell the encoder to generate +PLT markers. Security ======== From 7d6ff23e1f493147057f062f95954b21e9eec0fb Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 28 Mar 2023 07:32:30 +1100 Subject: [PATCH 441/727] Renamed "add_plt" to "plt" Co-authored-by: Hugo van Kemenade --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 11380cd555c..de6b793714c 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -595,7 +595,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.5.0 -**add_plt** +**plt** If ``True`` then include a PLT (packet length, tile-part header) marker segment in the produced file. The default is to not include it. From 598216fb465682646a307b930047d56adb0a42b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 19:18:50 +1100 Subject: [PATCH 442/727] OpenJPEG 2.4.0 or later is required for PLT markers --- docs/handbook/image-file-formats.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index de6b793714c..74ba883b15e 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -596,9 +596,9 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.5.0 **plt** - If ``True`` then include a PLT (packet length, tile-part header) marker - segment in the produced file. - The default is to not include it. + If ``True`` and OpenJPEG 2.4.0 or later is available, then include a PLT + (packet length, tile-part header) marker in the produced file. + Defaults to ``False``. .. versionadded:: 9.5.0 From 2f66d2d6a1dd307e8c28ddc5f98707acc5874cdb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 19:19:05 +1100 Subject: [PATCH 443/727] Changed maximum comment length to 65531 --- Tests/test_file_jpeg2k.py | 10 +++++++--- src/encode.c | 4 ++-- src/libImaging/Jpeg2KEncode.c | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 7b512695b92..b9422c76ac2 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -376,14 +376,18 @@ def test_save_comment(): for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) - out.seek(0) with Image.open(out) as im: assert im.info["comment"] == b"Created by Pillow" - too_long_comment = " " * 65531 + out = BytesIO() + long_comment = b" " * 65531 + test_card.save(out, "JPEG2000", comment=long_comment) + with Image.open(out) as im: + assert im.info["comment"] == long_comment + with pytest.raises(ValueError): - test_card.save(out, "JPEG2000", comment=too_long_comment) + test_card.save(out, "JPEG2000", comment=long_comment + b" ") @pytest.mark.parametrize( diff --git a/src/encode.c b/src/encode.c index 8aa357b6ca4..a665949357f 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,7 +1214,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; - char *comment = NULL; + char *comment; Py_ssize_t comment_size; int plt = 0; @@ -1323,7 +1323,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (comment && comment_size > 0) { /* Size is stored as as an uint16, subtract 4 bytes for the header */ - if (comment_size >= 65531) { + if (comment_size >= 65532) { PyErr_SetString( PyExc_ValueError, "JPEG 2000 comment is too long"); diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index a7c6441979b..8f637006160 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -503,7 +503,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ #if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) if (context->plt) { - const char * plt_option[2] = {"PLT=YES", NULL}; + const char *plt_option[2] = {"PLT=YES", NULL}; opj_encoder_set_extra_options(codec, plt_option); } #endif From 9a7a4482195125f38c668df3555043e8d3251da0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 20:14:29 +1100 Subject: [PATCH 444/727] Increase similiarity between test_plt_marker and _parse_comment --- Tests/test_file_jpeg2k.py | 21 +++++++++++---------- src/PIL/Jpeg2KImagePlugin.py | 8 ++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b9422c76ac2..52e9f8853b9 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -411,29 +411,30 @@ def test_crashes(test_file): @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): - # Search the start of the codesteam for the PLT box (id 0xFF58) + # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: - box_bytes = out.read(2) - if not box_bytes: + marker = out.read(2) + if not marker: # End of steam encountered and no PLT or SOD break - jp2_boxid = _binary.i16be(box_bytes) + jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: - # No length specifier for main header + # SOC has no length continue elif jp2_boxid == 0xFF58: - # This is the PLT box we're looking for + # PLT return elif jp2_boxid == 0xFF93: - # SOD box encountered and no PLT, so it wasn't found + # SOD without finding PLT first break - jp2_boxlength = _binary.i16be(out.read(2)) - out.seek(jp2_boxlength - 2, os.SEEK_CUR) + hdr = out.read(2) + length = _binary.i16be(hdr) + out.seek(length - 2, os.SEEK_CUR) - # The PLT box wasn't found + # PLT wasn't found raise ValueError diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index e7d91c8186d..9309768bacf 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -17,7 +17,7 @@ import os import struct -from . import Image, ImageFile +from . import Image, ImageFile, _binary class BoxReader: @@ -99,7 +99,7 @@ def _parse_codestream(fp): count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" hdr = fp.read(2) - lsiz = struct.unpack(">H", hdr)[0] + lsiz = _binary.i16be(hdr) siz = hdr + fp.read(lsiz - 2) lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( ">HHIIIIIIIIH", siz @@ -258,7 +258,7 @@ def _open(self): def _parse_comment(self): hdr = self.fp.read(2) - length = struct.unpack(">H", hdr)[0] + length = _binary.i16be(hdr) self.fp.seek(length - 2, os.SEEK_CUR) while True: @@ -270,7 +270,7 @@ def _parse_comment(self): # Start of tile or end of codestream break hdr = self.fp.read(2) - length = struct.unpack(">H", hdr)[0] + length = _binary.i16be(hdr) if typ == 0x64: # Comment self.info["comment"] = self.fp.read(length - 2)[2:] From 6d3c1985e07dfa28ae14de5bf36c711bdfc9176d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 22:18:14 +1100 Subject: [PATCH 445/727] Assert false instead of raising an error --- Tests/test_file_jpeg2k.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 52e9f8853b9..b6e8215f729 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -418,8 +418,7 @@ def test_plt_marker(): while True: marker = out.read(2) if not marker: - # End of steam encountered and no PLT or SOD - break + assert False, "End of stream without PLT" jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: @@ -429,12 +428,8 @@ def test_plt_marker(): # PLT return elif jp2_boxid == 0xFF93: - # SOD without finding PLT first - break + assert False, "SOD without finding PLT first" hdr = out.read(2) length = _binary.i16be(hdr) out.seek(length - 2, os.SEEK_CUR) - - # PLT wasn't found - raise ValueError From d7dd44dde01ea0f98073197f07a07915166733d0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 23:46:06 +1100 Subject: [PATCH 446/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bbe47473de8..969f4be086c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Support custom comments and PLT markers when saving JPEG2000 images #6903 + [joshware, radarhere, hugovk] + - Load before getting size in __array_interface__ #7034 [radarhere] From 61d0c8f5230a3fdde4c601a73217acd386d1b3be Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 29 Mar 2023 10:30:20 -0500 Subject: [PATCH 447/727] change PSFile deprecation from 9.4.0 to 9.5.0 --- docs/deprecations.rst | 20 ++++++++++---------- docs/releasenotes/9.4.0.rst | 11 ----------- docs/releasenotes/9.5.0.rst | 9 ++++++--- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4a03890a327..5669d2827f8 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -80,16 +80,6 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 was reversed in Pillow 9.4.0 and those constants will now remain available. See :ref:`restored-image-constants` -PSFile -~~~~~~ - -.. deprecated:: 9.4.0 - -The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will -be removed in Pillow 11 (2024-10-15). This class was only made as a helper to -be used internally, so there is no replacement. If you need this functionality -though, it is a very short class that can easily be recreated in your own code. - ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ @@ -217,6 +207,16 @@ Use instead:: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") width, height = right - left, bottom - top +PSFile +~~~~~~ + +.. deprecated:: 9.5.0 + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + Removed features ---------------- diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index b7a63dd61e7..0af5bc8ca11 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,17 +1,6 @@ 9.4.0 ----- -Deprecations -============ - -PSFile -^^^^^^ - -The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will -be removed in Pillow 11 (2024-10-15). This class was only made as a helper to -be used internally, so there is no replacement. If you need this functionality -though, it is a very short class that can easily be recreated in your own code. - API Additions ============= diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 0b0e0dd2f10..585e790eaf2 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -12,10 +12,13 @@ TODO Deprecations ============ -TODO -^^^^ +PSFile +^^^^^^ -TODO +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. API Changes =========== From 0ea1184bcfefd1965670cf6d06ca9d44364460d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 30 Mar 2023 07:54:01 +1100 Subject: [PATCH 448/727] Free additional variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> --- src/_imaging.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index c715c36c2ca..b6a7557ffd5 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1250,6 +1250,7 @@ _histogram(ImagingObject *self, PyObject *args) { /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); if (list == NULL) { + ImagingHistogramDelete(h); return NULL; } for (i = 0; i < h->bands * 256; i++) { @@ -2158,6 +2159,7 @@ _getcolors(ImagingObject *self, PyObject *args) { } else { out = PyList_New(colors); if (out == NULL) { + free(items); return NULL; } for (i = 0; i < colors; i++) { From 7632d8df3642111bd1b20b0863511a2ff6b3a113 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Mar 2023 12:35:07 +1100 Subject: [PATCH 449/727] Do not DECREF individual list items, reverting grouping --- src/_imagingft.c | 59 ++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 57f765e133d..92cfb1db00d 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1114,7 +1114,7 @@ font_getvarnames(FontObject *self) { static PyObject * font_getvaraxes(FontObject *self) { - int error, failed = 0; + int error; FT_UInt i, j, num_axis, name_count; FT_MM_Var *master; FT_Var_Axis axis; @@ -1137,46 +1137,35 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); if (list_axis == NULL) { - failed = 1; - } else { - PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); - PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None); - Py_XDECREF(minimum); + Py_DECREF(list_axes); + return NULL; + } + PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); + PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None); + Py_XDECREF(minimum); - PyObject *def = PyLong_FromLong(axis.def / 65536); - PyDict_SetItemString(list_axis, "default", def ? def : Py_None); - Py_XDECREF(def); + PyObject *def = PyLong_FromLong(axis.def / 65536); + PyDict_SetItemString(list_axis, "default", def ? def : Py_None); + Py_XDECREF(def); - PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); - PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); - Py_XDECREF(maximum); + PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); + PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); + Py_XDECREF(maximum); - for (j = 0; j < name_count; j++) { - error = FT_Get_Sfnt_Name(self->face, j, &name); - if (error) { - Py_DECREF(list_axis); - failed = 1; - break; - } - - if (name.name_id == axis.strid) { - axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); - Py_XDECREF(axis_name); - break; - } - } - } - if (failed) { - for (j = 0; j < i; j++) { - list_axis = PyList_GetItem(list_axes, j); - Py_DECREF(list_axis); - } - Py_DECREF(list_axes); + for (j = 0; j < name_count; j++) { + error = FT_Get_Sfnt_Name(self->face, j, &name); if (error) { + Py_DECREF(list_axis); + Py_DECREF(list_axes); return geterror(error); } - return NULL; + + if (name.name_id == axis.strid) { + axis_name = Py_BuildValue("y#", name.string, name.string_len); + PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); + Py_XDECREF(axis_name); + break; + } } PyList_SetItem(list_axes, i, list_axis); From 448ab0a68780a32b754eb1edb42b4b45d85491af Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 30 Mar 2023 14:36:58 +1100 Subject: [PATCH 450/727] Call FT_Done_MM_Var when returning early MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> --- src/_imagingft.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 92cfb1db00d..e0c28986570 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1083,6 +1083,7 @@ font_getvarnames(FontObject *self) { num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); if (list_names == NULL) { + FT_Done_MM_Var(library, master); return NULL; } @@ -1091,6 +1092,7 @@ font_getvarnames(FontObject *self) { error = FT_Get_Sfnt_Name(self->face, i, &name); if (error) { Py_DECREF(list_names); + FT_Done_MM_Var(library, master); return geterror(error); } @@ -1130,6 +1132,7 @@ font_getvaraxes(FontObject *self) { list_axes = PyList_New(num_axis); if (list_axes == NULL) { + FT_Done_MM_Var(library, master); return NULL; } for (i = 0; i < num_axis; i++) { @@ -1138,6 +1141,7 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); if (list_axis == NULL) { Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); return NULL; } PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); @@ -1157,6 +1161,7 @@ font_getvaraxes(FontObject *self) { if (error) { Py_DECREF(list_axis); Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); return geterror(error); } From c3364a424516b365bc2504b24faa5d223066ee26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Mar 2023 16:55:18 +1100 Subject: [PATCH 451/727] Do not use absolute path for ldconfig --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f8bcf3e3966..9f38170b564 100755 --- a/setup.py +++ b/setup.py @@ -166,14 +166,14 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ["/sbin/ldconfig", "-p"] + args = ["ldconfig", "-p"] expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ["/sbin/ldconfig", "-r"] + args = ["ldconfig", "-r"] expr = r".* => (.*)" env = {} From 17a0a2ee3eeb9df6e9fcf894d204911c7e6e4eef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 06:14:35 +1100 Subject: [PATCH 452/727] Removed unnecessary silencing of stderr --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 9f38170b564..07d6c66d655 100755 --- a/setup.py +++ b/setup.py @@ -273,9 +273,7 @@ def _pkg_config(name): )[::2][1:] cflags = re.split( r"(^|\s+)-I", - subprocess.check_output(command_cflags, stderr=stderr) - .decode("utf8") - .strip(), + subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags except Exception: From 1f0573f2d61ae7e79f3e86481449765cb794e5f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 06:38:10 +1100 Subject: [PATCH 453/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 969f4be086c..f10daf00441 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Removed absolute path to ldconfig #7044 + [radarhere] + - Support custom comments and PLT markers when saving JPEG2000 images #6903 [joshware, radarhere, hugovk] From b18efc775d5de4736e29b938a56325a9ab785b4e Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 31 Mar 2023 01:48:17 +0200 Subject: [PATCH 454/727] do not discard ImportError message in ImageFont --- src/PIL/ImageFont.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 30f6694e6cd..e3f72f52e7c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -54,17 +54,12 @@ def __getattr__(name): raise AttributeError(msg) -class _ImagingFtNotInstalled: - # module placeholder - def __getattr__(self, id): - msg = "The _imagingft C module is not installed" - raise ImportError(msg) - - try: from . import _imagingft as core -except ImportError: - core = _ImagingFtNotInstalled() +except ImportError as ex: + from ._util import DeferredError + + core = DeferredError(ex) _UNSPECIFIED = object() From e97167401104c75e04e7966978d0388886314dd2 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 31 Mar 2023 02:08:58 +0200 Subject: [PATCH 455/727] cleanup in _imagingft --- src/_imagingft.c | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 4f44d6a7158..c0082b4a76f 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -33,12 +33,6 @@ #include FT_COLOR_H #endif -#define KEEP_PY_UNICODE - -#if !defined(FT_LOAD_TARGET_MONO) -#define FT_LOAD_TARGET_MONO FT_LOAD_MONOCHROME -#endif - /* -------------------------------------------------------------------- */ /* error table */ @@ -420,11 +414,9 @@ text_layout_fallback( if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif for (i = 0; font_getchar(string, i, &ch); i++) { (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); @@ -581,11 +573,9 @@ font_getsize(FontObject *self, PyObject *args) { if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif /* * text bounds are given by: @@ -844,11 +834,9 @@ font_render(FontObject *self, PyObject *args) { if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif /* * calculate x_min and y_max @@ -958,13 +946,11 @@ font_render(FontObject *self, PyObject *args) { /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ case FT_PIXEL_MODE_GRAY: break; -#ifdef FT_LOAD_COLOR case FT_PIXEL_MODE_BGRA: if (color) { break; } /* we didn't ask for color, fall through to default */ -#endif default: PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; @@ -995,7 +981,6 @@ font_render(FontObject *self, PyObject *args) { } else { target = im->image8[yy] + xx; } -#ifdef FT_LOAD_COLOR if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { /* paste color glyph */ for (k = x0; k < x1; k++) { @@ -1010,9 +995,7 @@ font_render(FontObject *self, PyObject *args) { target[k * 4 + 3] = source[k * 4 + 3]; } } - } else -#endif - if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { if (color) { unsigned char *ink = (unsigned char *)&foreground_ink; for (k = x0; k < x1; k++) { From 3d4e9b107d22a3a3c4dee6fb20eb25d6f2639da8 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 31 Mar 2023 02:57:58 +0200 Subject: [PATCH 456/727] warn if module is found but fails to import in PIL.features --- src/PIL/features.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 6f9d99e765a..80a16a75e0c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -33,7 +33,10 @@ def check_module(feature): try: __import__(module) return True - except ImportError: + except ModuleNotFoundError: + return False + except ImportError as ex: + warnings.warn(str(ex)) return False @@ -145,7 +148,10 @@ def check_feature(feature): try: imported_module = __import__(module, fromlist=["PIL"]) return getattr(imported_module, flag) - except ImportError: + except ModuleNotFoundError: + return None + except ImportError as ex: + warnings.warn(str(ex)) return None From e95b55acd4d600b793f069c3c1fb266ec9ebe202 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 20:48:14 +1100 Subject: [PATCH 457/727] Document loss of palette when converting to NumPy --- src/PIL/Image.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 95f5a9bc175..cc0b90b1dc5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3031,21 +3031,25 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): def fromarray(obj, mode=None): """ Creates an image memory from an object exporting the array interface - (using the buffer protocol). + (using the buffer protocol):: + + from PIL import Image + import numpy as np + a = np.zeros((5, 5)) + im = Image.fromarray(a) If ``obj`` is not contiguous, then the ``tobytes`` method is called and :py:func:`~PIL.Image.frombuffer` is used. - If you have an image in NumPy:: + Pillow images can also be converted to arrays:: from PIL import Image import numpy as np im = Image.open("hopper.jpg") a = np.asarray(im) - Then this can be used to convert it to a Pillow image:: - - im = Image.fromarray(a) + When converting Pillow images to arrays however, only pixel values are + transferred. This means that P and PA mode images will lose their palette. :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Will be determined from From 485532c1f3c3cf146a37b88f81dd6a0ff077daf1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 21:00:28 +1100 Subject: [PATCH 458/727] Mention available pixel types when converting from NumPy --- src/PIL/Image.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cc0b90b1dc5..259616e2b16 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3041,6 +3041,10 @@ def fromarray(obj, mode=None): If ``obj`` is not contiguous, then the ``tobytes`` method is called and :py:func:`~PIL.Image.frombuffer` is used. + In the case of NumPy, be aware that Pillow modes do not always correspond + to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, + 32-signed integer pixels and 32-bit floating point pixels. + Pillow images can also be converted to arrays:: from PIL import Image From d84e227204bf95414481a933ef3299540c066f2f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 21:52:37 +1100 Subject: [PATCH 459/727] Fixed warning that variable may be uninitialized --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 728adc07bd7..d7b90b9e84c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -502,7 +502,7 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - int tupleSize; + int tupleSize = 0; if (PyTuple_Check(color)) { tupleSize = PyTuple_GET_SIZE(color); if (tupleSize == 1) { From 59d67fa68a2e0574fd6bc9047e1dea36318b6117 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 21:59:06 +1100 Subject: [PATCH 460/727] Only call PyTuple_Check once in getink --- src/_imaging.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index d7b90b9e84c..281f3a4d2e6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -502,12 +502,9 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - int tupleSize = 0; - if (PyTuple_Check(color)) { - tupleSize = PyTuple_GET_SIZE(color); - if (tupleSize == 1) { - color = PyTuple_GetItem(color, 0); - } + int tupleSize = PyTuple_Check(color) ? PyTuple_GET_SIZE(color) : -1; + if (tupleSize == 1) { + color = PyTuple_GetItem(color, 0); } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || im->type == IMAGING_TYPE_SPECIAL) { @@ -521,7 +518,7 @@ getink(PyObject *color, Imaging im, char *ink) { PyErr_SetString( PyExc_TypeError, "color must be int or single-element tuple"); return NULL; - } else if (!PyTuple_Check(color)) { + } else if (tupleSize == -1) { PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); return NULL; } From 89d2cdfcfa21f9a13499ef19d4fb9514a02691fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 22:30:36 +1100 Subject: [PATCH 461/727] Fixed warning that nLeft is set but not used --- src/libImaging/Quant.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 783852c24cf..02a4a5c7668 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -341,7 +341,10 @@ splitlists( PixelList *l, *r, *c, *n; int i; - int nRight, nLeft; + int nRight; +#ifndef NO_OUTPUT + int nLeft; +#endif int splitColourVal; #ifdef TEST_SPLIT @@ -396,12 +399,17 @@ splitlists( } #endif nCount[0] = nCount[1] = 0; - nLeft = nRight = 0; + nRight = 0; +#ifndef NO_OUTPUT + nLeft = 0; +#endif for (left = 0, c = h[axis]; c;) { left = left + c->count; nCount[0] += c->count; c->flag = 0; +#ifndef NO_OUTPUT nLeft++; +#endif c = c->next[axis]; if (left * 2 > pixelCount) { break; @@ -414,7 +422,9 @@ splitlists( break; } c->flag = 0; +#ifndef NO_OUTPUT nLeft++; +#endif nCount[0] += c->count; } } @@ -430,7 +440,9 @@ splitlists( } c->flag = 1; nRight++; +#ifndef NO_OUTPUT nLeft--; +#endif nCount[0] -= c->count; nCount[1] += c->count; } From b606da7f0eb73e2f40af3a162c7c4fc19e6a2723 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 31 Mar 2023 07:19:33 -0500 Subject: [PATCH 462/727] add missing word --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 259616e2b16..4a142a008ff 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3043,7 +3043,7 @@ def fromarray(obj, mode=None): In the case of NumPy, be aware that Pillow modes do not always correspond to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, - 32-signed integer pixels and 32-bit floating point pixels. + 32-bit signed integer pixels, and 32-bit floating point pixels. Pillow images can also be converted to arrays:: From 5932a0bd19b513c3c86d9fc8dec5d41384247056 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 09:23:57 +1100 Subject: [PATCH 463/727] Clear half token after use --- Tests/test_file_ppm.py | 10 ++++++++++ docs/releasenotes/9.5.0.rst | 15 ++++++++++++--- src/PIL/PpmImagePlugin.py | 1 + 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fbcbea6c691..292642ca9f8 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -256,6 +256,16 @@ def test_truncated_file(tmp_path): im.load() +def test_not_enough_image_data(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P2 1 2 255 255") + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + @pytest.mark.parametrize("maxval", (b"0", b"65536")) def test_invalid_maxval(maxval, tmp_path): path = str(tmp_path / "temp.ppm") diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 0b0e0dd2f10..1ba9b98905b 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -62,10 +62,19 @@ PLT markers. Security ======== -TODO -^^^^ +Clear PPM half token after use +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Image files that are small on disk are often prevented from expanding to be +big images consuming a large amount of resources simply because they lack the +data to populate those resources. + +PpmImagePlugin might hold onto the last data read for a pixel value in case the +pixel value has not been finished yet. However, that data was not being cleared +afterwards, meaning that infinite data could be available to fill any image +size. + +That data is now cleared after use. Other Changes ============= diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 5aa418044b1..2cb1e56365d 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -237,6 +237,7 @@ def _decode_blocks(self, maxval): if half_token: block = half_token + block # stitch half_token to new block + half_token = False tokens = block.split() From aa9ecac0328a7dce7d7f3bd3d0ee7a9ec0316f83 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 10:27:39 +1100 Subject: [PATCH 464/727] Added ImageSourceData to TAGS_V2 --- Tests/test_file_libtiff.py | 5 +++++ docs/releasenotes/9.5.0.rst | 10 +++++++++- src/PIL/TiffTags.py | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 7a94c0302f9..53cfd81f702 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -668,6 +668,11 @@ def test_save_ycbcr(self, tmp_path): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) + def test_save_imagesourcedata(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + im.save(outfile) + def test_crashing_metadata(self, tmp_path): # issue 1597 with Image.open("Tests/images/rdf.tif") as im: diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 1ba9b98905b..9157210827c 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -72,10 +72,18 @@ data to populate those resources. PpmImagePlugin might hold onto the last data read for a pixel value in case the pixel value has not been finished yet. However, that data was not being cleared afterwards, meaning that infinite data could be available to fill any image -size. +size. This has been present since Pillow 9.2.0. That data is now cleared after use. +Saving TIFF tag ImageSourceData +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If Pillow incorrectly saved the TIFF tag ImageSourceData as ASCII instead of +UNDEFINED, a segmentation fault was triggered. + +The correct tag type will now be used by default instead. + Other Changes ============= diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index ac048ba562f..30b05e4e1d4 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -195,6 +195,7 @@ def lookup(tag, group=None): 34675: ("ICCProfile", UNDEFINED, 1), 34853: ("GPSInfoIFD", LONG, 1), 36864: ("ExifVersion", UNDEFINED, 1), + 37724: ("ImageSourceData", UNDEFINED, 1), 40965: ("InteroperabilityIFD", LONG, 1), 41730: ("CFAPattern", UNDEFINED, 1), # MPInfo From b1b0353d17bcdca99cfcb2ea48c6af7861fb43ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 12:21:16 +1100 Subject: [PATCH 465/727] Corrected passing TIFF_LONG to libtiff --- Tests/test_file_libtiff.py | 7 ++++++- src/encode.c | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 53cfd81f702..ac78b086965 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -668,11 +668,16 @@ def test_save_ycbcr(self, tmp_path): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) - def test_save_imagesourcedata(self, tmp_path): + def test_exif_ifd(self, tmp_path): outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert im.tag_v2[34665] == 125456 im.save(outfile) + with Image.open(outfile) as reloaded: + if Image.core.libtiff_support_custom_tags: + assert reloaded.tag_v2[34665] == 125456 + def test_crashing_metadata(self, tmp_path): # issue 1597 with Image.open("Tests/images/rdf.tif") as im: diff --git a/src/encode.c b/src/encode.c index a665949357f..308bd2059cc 100644 --- a/src/encode.c +++ b/src/encode.c @@ -904,7 +904,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)); + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value)); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value)); From ac7878082e4caea5fea5ac4db26425e53ffa037c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 14:06:42 +1100 Subject: [PATCH 466/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f10daf00441..43978bea4e6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added ImageSourceData to TAGS_V2 #7053 + [radarhere] + +- Clear PPM half token after use #7052 + [radarhere] + - Removed absolute path to ldconfig #7044 [radarhere] From 10794e0d66e253868ecede1828282247d71bc7ac Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Apr 2023 09:15:01 +0300 Subject: [PATCH 467/727] 9.5.0 version bump --- CHANGES.rst | 2 +- src/PIL/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 43978bea4e6..b77017f8ac8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changelog (Pillow) ================== -9.5.0 (unreleased) +9.5.0 (2023-04-01) ------------------ - Added ImageSourceData to TAGS_V2 #7053 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 7baa9fb6c55..d94d3593440 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.5.0.dev0" +__version__ = "9.5.0" From fa689fba04577830eba4f4f4f923d55f9c4bc833 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 18:35:43 +1100 Subject: [PATCH 468/727] Removed unused sections --- docs/releasenotes/9.5.0.rst | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index bda3cd05a2b..b1e982fccff 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -1,14 +1,6 @@ 9.5.0 ----- -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -TODO - Deprecations ============ @@ -20,14 +12,6 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. -API Changes -=========== - -TODO -^^^^ - -TODO - API Additions ============= From 2203afeafa76519fcc01682d8c35e5c5d569d7c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 19:36:06 +1100 Subject: [PATCH 469/727] Do not set size unnecessarily if image failed to open --- src/PIL/EpsImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c749..2f7fee9012a 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -354,7 +354,6 @@ def check_required_header_comments(): check_required_header_comments() if not self._size: - self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) From 4f7070e24c9cba8fc7ffab8d9f1c291977bbf183 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Apr 2023 12:34:27 +0300 Subject: [PATCH 470/727] 9.6.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index d94d3593440..c5c37fe91d6 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.5.0" +__version__ = "9.6.0.dev0" From 1e250e1137171101e88cd1e1fbd2962e967b4aac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 23:36:52 +1100 Subject: [PATCH 471/727] 10.0.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index c5c37fe91d6..800203d5170 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.6.0.dev0" +__version__ = "10.0.0.dev0" From 596569c928610a42f2f75a5c3b259d961de62ebf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Apr 2023 15:58:08 +0300 Subject: [PATCH 472/727] Drop support for soon-EOL Python 3.7 --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 3 +-- .pre-commit-config.yaml | 2 +- Makefile | 2 +- docs/installation.rst | 8 ++++---- docs/newer-versions.csv | 3 ++- setup.cfg | 3 +-- tox.ini | 2 +- 9 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b5913e04386..9ed192e0f37 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,7 +13,7 @@ environment: - PYTHON: C:/Python311 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - - PYTHON: C:/Python37-x64 + - PYTHON: C:/Python38-x64 ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 @@ -31,7 +31,7 @@ install: - path c:\nasm-2.15.05;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | - c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ + c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\pillow\winbuild\build\build_dep_all.cmd $host.SetShouldExit(0) - path C:\pillow\winbuild\build\bin;%PATH% diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ba72cb7b82f..8f4d53ecf06 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 10c3cd929f8..fced6113b50 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,10 +36,9 @@ jobs: "3.10", "3.9", "3.8", - "3.7", ] include: - - python-version: "3.7" + - python-version: "3.9" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - python-version: "3.8" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45c1f3c5f08..b3cda996507 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 23.1.0 hooks: - id: black - args: [--target-version=py37] + args: [--target-version=py38] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] diff --git a/Makefile b/Makefile index bb0ea60b35a..f51325d474b 100644 --- a/Makefile +++ b/Makefile @@ -123,5 +123,5 @@ lint: lint-fix: python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort - python3 -m black --target-version py37 . + python3 -m black --target-version py38 . python3 -m isort . diff --git a/docs/installation.rst b/docs/installation.rst index 9ec15a8f1de..4f12a5713e9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -454,22 +454,22 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.10 | arm64v8, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2016 | 3.7 | x86-64 | +| Windows Server 2016 | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | +| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index ed2369259d4..d53947ff5c9 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,5 +1,6 @@ Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,, +Pillow >= 10,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,Yes,Yes,Yes,Yes,Yes,, Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, diff --git a/setup.cfg b/setup.cfg index d6057f1599d..06e95d7cc2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,6 @@ classifiers = License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND) Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -36,7 +35,7 @@ project_urls = [options] packages = PIL -python_requires = >=3.7 +python_requires = >=3.8 include_package_data = True package_dir = = src diff --git a/tox.ini b/tox.ini index 9a41ca96b74..95a6a456379 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = lint - py{py3, 311, 310, 39, 38, 37} + py{py3, 311, 310, 39, 38} minversion = 1.9 [testenv] From b399ebc8c2fbe5e9be5883fac2cd7f3918d98100 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Apr 2023 17:02:39 +0300 Subject: [PATCH 473/727] Bump Python on RTD from 3.7 to 3.11 --- .readthedocs.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 0f581ebba90..98d9e4425ac 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,10 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.11" + python: install: - method: pip From 6a2087ebe40ca70800143f8d3a9793a0267b5b3e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 17:46:00 +0300 Subject: [PATCH 474/727] Amazon Linux 2 now tested with Python 3.9 --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 4f12a5713e9..7088657f905 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -434,7 +434,7 @@ These platforms are built and tested for every change. +==================================+============================+=====================+ | Alpine | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2 | 3.7 | x86-64 | +| Amazon Linux 2 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2023 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ From 00d18fc100011613f8f1c114293b2326065790f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Apr 2023 07:39:31 +1000 Subject: [PATCH 475/727] Updated Python version in documentation --- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 21b40d4e671..2975acf283f 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -18,7 +18,7 @@ The following is a simplified version of the script used on AppVeyor: ``` set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild -C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends +C:\Python39\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends build\build_dep_all.cmd build\build_pillow.cmd install cd .. diff --git a/winbuild/build.rst b/winbuild/build.rst index e83045f0cf8..99dfad3015f 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -112,7 +112,7 @@ The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild - C:\Python37\bin\python.exe build_prepare.py -v --depends C:\pillow-depends + C:\Python39\bin\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd build\build_pillow.cmd install cd .. From fada40d2e8f613cb040aa7465d40c17db8e37069 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Apr 2023 19:29:09 +1000 Subject: [PATCH 476/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b77017f8ac8..547893d8250 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +10.0.0 (unreleased) +------------------- + +- Drop support for soon-EOL Python 3.7 #7058 + [hugovk, radarhere] + 9.5.0 (2023-04-01) ------------------ From 5a7e2ad638d1b3cb8d34006d47154d35b2df9c74 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 19:44:19 +0000 Subject: [PATCH 477/727] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0) - [github.com/PyCQA/bandit: 1.7.4 → 1.7.5](https://github.com/PyCQA/bandit/compare/1.7.4...1.7.5) - [github.com/Lucas-C/pre-commit-hooks: v1.4.2 → v1.5.1](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.4.2...v1.5.1) - [github.com/tox-dev/tox-ini-fmt: 0.6.1 → 1.0.0](https://github.com/tox-dev/tox-ini-fmt/compare/0.6.1...1.0.0) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3cda996507..51c7117d7ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black args: [--target-version=py38] @@ -14,7 +14,7 @@ repos: - id: isort - repo: https://github.com/PyCQA/bandit - rev: 1.7.4 + rev: 1.7.5 hooks: - id: bandit args: [--severity-level=high] @@ -26,7 +26,7 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.4.2 + rev: v1.5.1 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) @@ -57,7 +57,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.6.1 + rev: 1.0.0 hooks: - id: tox-ini-fmt From f46594ff47f3401d535588ee9b3f2259d3c387d3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 3 Apr 2023 23:07:45 +0300 Subject: [PATCH 478/727] Don't remove tabs from hopper.gd --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51c7117d7ce..c3b6dc0a6ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: rev: v1.5.1 hooks: - id: remove-tabs - exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 From 6c057b32771cd24e76656ca5b92a107e43eb08f8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 3 Apr 2023 23:08:23 +0300 Subject: [PATCH 479/727] Run tox-ini-fmt --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 95a6a456379..d7948ef6dfb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] +minversion = 1.9 envlist = lint py{py3, 311, 310, 39, 38} -minversion = 1.9 [testenv] deps = @@ -15,15 +15,16 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -allowlist_externals = make +allowlist_externals = + make [testenv:lint] -passenv = - PRE_COMMIT_COLOR skip_install = true deps = check-manifest pre-commit +passenv = + PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure check-manifest From 498b475e82105fcad0b78465dd259973849fbd55 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Apr 2023 13:23:19 +1000 Subject: [PATCH 480/727] Added links to more information about the enhancement factor --- docs/reference/ImageEnhance.rst | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index b27228ec924..746f18cd2c0 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -29,6 +29,8 @@ Classes All enhancement classes implement a common interface, containing a single method: +.. _enhancement-factor: + .. py:class:: _Enhance .. py:method:: enhance(factor) @@ -45,31 +47,33 @@ method: Adjust image color balance. - This class can be used to adjust the colour balance of an image, in - a manner similar to the controls on a colour TV set. An enhancement - factor of 0.0 gives a black and white image. A factor of 1.0 gives - the original image. + This class can be used to adjust the colour balance of an image, in a + manner similar to the controls on a colour TV set. An + :ref:`enhancement factor ` of 0.0 gives a black and + white image. A factor of 1.0 gives the original image. .. py:class:: Contrast(image) Adjust image contrast. - This class can be used to control the contrast of an image, similar - to the contrast control on a TV set. An enhancement factor of 0.0 - gives a solid grey image. A factor of 1.0 gives the original image. + This class can be used to control the contrast of an image, similar to the + contrast control on a TV set. An + :ref:`enhancement factor ` of 0.0 gives a solid grey + image. A factor of 1.0 gives the original image. .. py:class:: Brightness(image) Adjust image brightness. - This class can be used to control the brightness of an image. An - enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the - original image. + This class can be used to control the brightness of an image. An + :ref:`enhancement factor ` of 0.0 gives a black image. + A factor of 1.0 gives the original image. .. py:class:: Sharpness(image) Adjust image sharpness. This class can be used to adjust the sharpness of an image. An - enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the - original image, and a factor of 2.0 gives a sharpened image. + :ref:`enhancement factor ` of 0.0 gives a blurred + image, a factor of 1.0 gives the original image, and a factor of 2.0 gives + a sharpened image. From 4720774a8523e69bbca5b9fb71c1f38c0ba38151 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Apr 2023 14:59:26 +1000 Subject: [PATCH 481/727] Describe the effect of brightness and contrast factors above 1 --- docs/reference/ImageEnhance.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 746f18cd2c0..457f0d4df1b 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -59,15 +59,17 @@ method: This class can be used to control the contrast of an image, similar to the contrast control on a TV set. An :ref:`enhancement factor ` of 0.0 gives a solid grey - image. A factor of 1.0 gives the original image. + image, a factor of 1.0 gives the original image, and greater values + increase the contrast of the image. .. py:class:: Brightness(image) Adjust image brightness. This class can be used to control the brightness of an image. An - :ref:`enhancement factor ` of 0.0 gives a black image. - A factor of 1.0 gives the original image. + :ref:`enhancement factor ` of 0.0 gives a black image, + a factor of 1.0 gives the original image, and greater values increase the + brightness of the image. .. py:class:: Sharpness(image) From ca2bf046d35ec41251716cac152fc9209964a359 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Apr 2023 09:57:16 +1000 Subject: [PATCH 482/727] Use "/sbin/ldconfig" if ldconfig is not found --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 07d6c66d655..f9670d4c040 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ import os import re +import shutil import struct import subprocess import sys @@ -150,6 +151,7 @@ def _dbg(s, tp=None): def _find_library_dirs_ldconfig(): # Based on ctypes.util from Python 2 + ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" @@ -166,14 +168,14 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ["ldconfig", "-p"] + args = [ldconfig, "-p"] expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ["ldconfig", "-r"] + args = [ldconfig, "-r"] expr = r".* => (.*)" env = {} From d94239ae3d21d8ae03f5120228dc8225faa99bac Mon Sep 17 00:00:00 2001 From: adisbladis Date: Wed, 5 Apr 2023 15:26:46 +1200 Subject: [PATCH 483/727] Handle polymorphic types for lib_root and include_root in setup.py Depending on whether these are created by pkg_config or not they might be a list of directories or just a string with a single directory. --- setup.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 07d6c66d655..d780b038ade 100755 --- a/setup.py +++ b/setup.py @@ -473,11 +473,17 @@ def build_extensions(self): lib_root = include_root = root if lib_root is not None: - for lib_dir in lib_root: - _add_directory(library_dirs, lib_dir) + if isinstance(lib_root, str): + _add_directory(library_dirs, lib_root) + else: + for lib_dir in lib_root: + _add_directory(library_dirs, lib_dir) if include_root is not None: - for include_dir in include_root: - _add_directory(include_dirs, include_dir) + if isinstance(include_root, str): + _add_directory(include_dirs, include_root) + else: + for include_dir in include_root: + _add_directory(include_dirs, include_dir) # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): From b2b660e2c0b6f209916a2bd88b22f650a86fabce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Apr 2023 18:53:28 +1000 Subject: [PATCH 484/727] Removed FIXME comment --- setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.py b/setup.py index 07d6c66d655..589a1471e6e 100755 --- a/setup.py +++ b/setup.py @@ -681,10 +681,6 @@ def build_extensions(self): # Add the directory to the include path so we can include # rather than having to cope with the versioned # include path - # FIXME (melvyn-sopacua): - # At this point it's possible that best_path is already in - # self.compiler.include_dirs. Should investigate how that is - # possible. _add_directory(self.compiler.include_dirs, best_path, 0) feature.jpeg2000 = "openjp2" feature.openjpeg_version = ".".join(str(x) for x in best_version) From 59c9d87f8a4d8443a482ca7abb5f1f8d5d970cbd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 00:27:22 +0300 Subject: [PATCH 485/727] Remove support for PyQt5 and PySide2, deprecated in 9.2.0 --- .github/workflows/test-cygwin.yml | 2 +- Tests/test_deprecated_imageqt.py | 18 ------------------ Tests/test_imageqt.py | 11 ++--------- Tests/test_qt_image_qapplication.py | 14 +------------- Tests/test_qt_image_toqimage.py | 6 +----- docs/deprecations.rst | 26 +++++++++++++------------- docs/reference/ImageQt.rst | 14 +++----------- src/PIL/ImageQt.py | 13 ------------- 8 files changed, 21 insertions(+), 83 deletions(-) delete mode 100644 Tests/test_deprecated_imageqt.py diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 6c9ed66e32b..397cfe0a187 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -67,7 +67,7 @@ jobs: python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools + qt6-devel-tools wget xorg-server-extra zlib-devel diff --git a/Tests/test_deprecated_imageqt.py b/Tests/test_deprecated_imageqt.py deleted file mode 100644 index 2528ff3f7d4..00000000000 --- a/Tests/test_deprecated_imageqt.py +++ /dev/null @@ -1,18 +0,0 @@ -import warnings - -with warnings.catch_warnings(record=True) as w: - # Arrange: cause all warnings to always be triggered - warnings.simplefilter("always") - - # Act: trigger a warning with Qt5 - from PIL import ImageQt - - -def test_deprecated(): - # Assert - if ImageQt.qt_version in ("5", "side2"): - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "deprecated" in str(w[0].message) - else: - assert len(w) == 0 diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 2f2b0791853..2c73a209465 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -2,12 +2,9 @@ import pytest -from .helper import assert_image_similar, hopper - -with warnings.catch_warnings() as w: - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt +from .helper import assert_image_similar, hopper pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" @@ -26,10 +23,6 @@ def test_rgb(): from PyQt6.QtGui import qRgb elif ImageQt.qt_version == "side6": from PySide6.QtGui import qRgb - elif ImageQt.qt_version == "5": - from PyQt5.QtGui import qRgb - elif ImageQt.qt_version == "side2": - from PySide2.QtGui import qRgb assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 4929fa93378..5d2e41212f2 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,10 +1,6 @@ -import warnings - import pytest -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -19,14 +15,6 @@ from PySide6.QtCore import QPoint from PySide6.QtGui import QImage, QPainter, QRegion from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "5": - from PyQt5.QtCore import QPoint - from PyQt5.QtGui import QImage, QPainter, QRegion - from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "side2": - from PySide2.QtCore import QPoint - from PySide2.QtGui import QImage, QPainter, QRegion - from PySide2.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): def __init__(self): diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index c1983031a14..399df670f61 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,10 +1,6 @@ -import warnings - import pytest -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt from .helper import assert_image_equal, assert_image_equal_tofile, hopper diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 5669d2827f8..48e8b6d93f6 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -135,19 +135,6 @@ PhotoImage.paste box parameter The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). -PyQt5 and PySide2 -~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. - Image.coerce_e ~~~~~~~~~~~~~~ @@ -223,6 +210,19 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +PyQt5 and PySide2 +~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + PILLOW_VERSION constant ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 15d052d1c4e..7e67a44d364 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,16 +4,8 @@ :py:mod:`~PIL.ImageQt` Module ============================= -The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5 -or PySide2 QImage objects from PIL images. - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. +The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 +QImage objects from PIL images. .. versionadded:: 1.1.6 @@ -22,7 +14,7 @@ in Pillow 10 (2023-07-01). Upgrade to Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt6/PySide6/PyQt5/PySide2 API functions and methods. + to PyQt6/PySide6 API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index ad607a97b1a..9b7245454df 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -20,14 +20,11 @@ from io import BytesIO from . import Image -from ._deprecate import deprecate from ._util import is_path qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], - ["5", "PyQt5"], - ["side2", "PySide2"], ] # If a version has already been imported, attempt it first @@ -40,16 +37,6 @@ elif qt_module == "PySide6": from PySide6.QtCore import QBuffer, QIODevice from PySide6.QtGui import QImage, QPixmap, qRgba - elif qt_module == "PyQt5": - from PyQt5.QtCore import QBuffer, QIODevice - from PyQt5.QtGui import QImage, QPixmap, qRgba - - deprecate("Support for PyQt5", 10, "PyQt6 or PySide6") - elif qt_module == "PySide2": - from PySide2.QtCore import QBuffer, QIODevice - from PySide2.QtGui import QImage, QPixmap, qRgba - - deprecate("Support for PySide2", 10, "PyQt6 or PySide6") except (ImportError, RuntimeError): continue qt_is_installed = True From 070e7704695f92ea829c678b58bb1ce5b8cbc51a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 19:32:26 +0300 Subject: [PATCH 486/727] Remove support for Tk/Tcl <= 8.4, deprecated in 8.2.0 --- docs/deprecations.rst | 16 +++++------ src/PIL/_tkinter_finder.py | 6 ---- src/Tk/_tkmini.h | 14 +-------- src/Tk/tkImaging.c | 58 +++++++++----------------------------- 4 files changed, 22 insertions(+), 72 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 48e8b6d93f6..47d1531aef6 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,14 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Tk/Tcl 8.4 -~~~~~~~~~~ - -.. deprecated:: 8.2.0 - -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -when Tk/Tcl 8.5 will be the minimum supported. - Categories ~~~~~~~~~~ @@ -210,6 +202,14 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +Tk/Tcl 8.4 +~~~~~~~~~~ + +.. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 + +Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01). + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 5cd7e9b1fb2..597c21b5e38 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -4,8 +4,6 @@ import tkinter from tkinter import _tkinter as tk -from ._deprecate import deprecate - try: if hasattr(sys, "pypy_find_executable"): TKINTER_LIB = tk.tklib_cffi.__file__ @@ -17,7 +15,3 @@ TKINTER_LIB = None tk_version = str(tkinter.TkVersion) -if tk_version == "8.4": - deprecate( - "Support for Tk/Tcl 8.4", 10, action="Please upgrade to Tk/Tcl 8.5 or newer" - ) diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h index 9852fc9d688..68247bc472d 100644 --- a/src/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -119,17 +119,7 @@ typedef struct Tk_PhotoImageBlock { } Tk_PhotoImageBlock; /* Typedefs derived from function signatures in Tk header */ -/* Tk_PhotoPutBlock for Tk <= 8.4 */ -typedef void (*Tk_PhotoPutBlock_84_t)( - Tk_PhotoHandle handle, - Tk_PhotoImageBlock *blockPtr, - int x, - int y, - int width, - int height, - int compRule); -/* Tk_PhotoPutBlock for Tk >= 8.5 */ -typedef int (*Tk_PhotoPutBlock_85_t)( +typedef int (*Tk_PhotoPutBlock_t)( Tcl_Interp *interp, Tk_PhotoHandle handle, Tk_PhotoImageBlock *blockPtr, @@ -138,8 +128,6 @@ typedef int (*Tk_PhotoPutBlock_85_t)( int width, int height, int compRule); -/* Tk_PhotoSetSize for Tk <= 8.4 */ -typedef void (*Tk_PhotoSetSize_84_t)(Tk_PhotoHandle handle, int width, int height); /* Tk_FindPhoto */ typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); /* Tk_PhotoGetImage */ diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index ad503baec61..bd3cafe9596 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -48,14 +48,11 @@ * Global vars for Tcl / Tk functions. We load these symbols from the tkinter * extension module or loaded Tcl / Tk libraries at run-time. */ -static int TK_LT_85 = 0; static Tcl_CreateCommand_t TCL_CREATE_COMMAND; static Tcl_AppendResult_t TCL_APPEND_RESULT; static Tk_FindPhoto_t TK_FIND_PHOTO; static Tk_PhotoGetImage_t TK_PHOTO_GET_IMAGE; -static Tk_PhotoPutBlock_84_t TK_PHOTO_PUT_BLOCK_84; -static Tk_PhotoSetSize_84_t TK_PHOTO_SET_SIZE_84; -static Tk_PhotoPutBlock_85_t TK_PHOTO_PUT_BLOCK_85; +static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging ImagingFind(const char *name) { @@ -130,26 +127,15 @@ PyImagingPhotoPut( block.pitch = im->linesize; block.pixelPtr = (unsigned char *)im->block; - if (TK_LT_85) { /* Tk 8.4 */ - TK_PHOTO_PUT_BLOCK_84( - photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); - if (strcmp(im->mode, "RGBA") == 0) { - /* Tk workaround: we need apply ToggleComplexAlphaIfNeeded */ - /* (fixed in Tk 8.5a3) */ - TK_PHOTO_SET_SIZE_84(photo, block.width, block.height); - } - } else { - /* Tk >=8.5 */ - TK_PHOTO_PUT_BLOCK_85( - interp, - photo, - &block, - 0, - 0, - block.width, - block.height, - TK_PHOTO_COMPOSITE_SET); - } + TK_PHOTO_PUT_BLOCK( + interp, + photo, + &block, + 0, + 0, + block.width, + block.height, + TK_PHOTO_COMPOSITE_SET); return TCL_OK; } @@ -290,16 +276,7 @@ get_tk(HMODULE hMod) { if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(hMod, "Tk_FindPhoto")) == NULL) { return -1; }; - TK_LT_85 = GetProcAddress(hMod, "Tk_PhotoPutBlock_Panic") == NULL; - /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ - if (TK_LT_85) { - TK_PHOTO_PUT_BLOCK_84 = (Tk_PhotoPutBlock_84_t)func; - return ((TK_PHOTO_SET_SIZE_84 = - (Tk_PhotoSetSize_84_t)_dfunc(hMod, "Tk_PhotoSetSize")) == NULL) - ? -1 - : 1; - } - TK_PHOTO_PUT_BLOCK_85 = (Tk_PhotoPutBlock_85_t)func; + TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)func; return 1; } @@ -422,18 +399,9 @@ _func_loader(void *lib) { if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(lib, "Tk_FindPhoto")) == NULL) { return 1; } - /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ - TK_LT_85 = (dlsym(lib, "Tk_PhotoPutBlock_Panic") == NULL); - if (TK_LT_85) { - return ( - ((TK_PHOTO_PUT_BLOCK_84 = - (Tk_PhotoPutBlock_84_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL) || - ((TK_PHOTO_SET_SIZE_84 = - (Tk_PhotoSetSize_84_t)_dfunc(lib, "Tk_PhotoSetSize")) == NULL)); - } return ( - (TK_PHOTO_PUT_BLOCK_85 = - (Tk_PhotoPutBlock_85_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); + (TK_PHOTO_PUT_BLOCK = + (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); } int From ddc4e902352e1d4459592707e78081c48ba4803a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 19:51:16 +0300 Subject: [PATCH 487/727] Remove im.category and related Image.NORMAL, Image.SEQUENCE, Image.CONTAINER, deprecated in 8.2.0 --- Tests/test_image.py | 11 ----------- docs/deprecations.rst | 24 ++++++++++++------------ src/PIL/Image.py | 10 ---------- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 17f1edb00d1..cb31565e775 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -929,17 +929,6 @@ def test_apply_transparency(self): im.apply_transparency() assert im.palette.colors[(27, 35, 6, 214)] == 24 - def test_categories_deprecation(self): - with pytest.warns(DeprecationWarning): - assert hopper().category == 0 - - with pytest.warns(DeprecationWarning): - assert Image.NORMAL == 0 - with pytest.warns(DeprecationWarning): - assert Image.SEQUENCE == 1 - with pytest.warns(DeprecationWarning): - assert Image.CONTAINER == 2 - def test_constants(self): with pytest.warns(DeprecationWarning): assert Image.LINEAR == Image.Resampling.BILINEAR diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 47d1531aef6..1e448ad3133 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,18 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Categories -~~~~~~~~~~ - -.. deprecated:: 8.2.0 - -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and -``Image.CONTAINER`` attributes. - -To determine if an image has multiple frames or not, -``getattr(im, "is_animated", False)`` can be used instead. - JpegImagePlugin.convert_dict_qtables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -210,6 +198,18 @@ Tk/Tcl 8.4 Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01). +Categories +~~~~~~~~~~ + +.. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 + +``im.category`` was removed along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4a142a008ff..99a895fb036 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -61,10 +61,6 @@ def __getattr__(name): - categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} - if name in categories: - deprecate("Image categories", 10, "is_animated", plural=True) - return categories[name] old_resampling = { "LINEAR": "BILINEAR", "CUBIC": "BICUBIC", @@ -521,12 +517,6 @@ def __init__(self): self.pyaccess = None self._exif = None - def __getattr__(self, name): - if name == "category": - deprecate("Image categories", 10, "is_animated", plural=True) - return self._category - raise AttributeError(name) - @property def width(self): return self.size[0] From 52f4fc59a23fb57159d0622226461bcf1be1058a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 20:27:30 +0300 Subject: [PATCH 488/727] Remove JpegImagePlugin.convert_dict_qtables, deprecated in 8.3.0 --- Tests/test_file_jpeg.py | 6 ------ docs/deprecations.rst | 19 +++++++++---------- docs/releasenotes/8.3.0.rst | 2 +- src/PIL/JpegImagePlugin.py | 6 ------ 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 4981e15aff9..73a00386f6f 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -636,12 +636,6 @@ def test_save_low_quality_baseline_qtables(self): assert max(im2.quantization[0]) <= 255 assert max(im2.quantization[1]) <= 255 - def test_convert_dict_qtables_deprecation(self): - with pytest.warns(DeprecationWarning): - qtable = {0: [1, 2, 3, 4]} - qtable2 = JpegImagePlugin.convert_dict_qtables(qtable) - assert qtable == qtable2 - @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 1e448ad3133..f70a91834b9 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,16 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -JpegImagePlugin.convert_dict_qtables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 8.3.0 - -JPEG ``quantization`` is now automatically converted, but still returned as a -dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer -performs any operations on the data given to it, has been deprecated and will be -removed in Pillow 10.0.0 (2023-07-01). - ImagePalette size parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -210,6 +200,15 @@ Categories To determine if an image has multiple frames or not, ``getattr(im, "is_animated", False)`` can be used instead. +JpegImagePlugin.convert_dict_qtables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.3.0 +.. versionremoved:: 10.0.0 + +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index b9642576f96..e74880f6f40 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -8,7 +8,7 @@ JpegImagePlugin.convert_dict_qtables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ JPEG ``quantization`` is now automatically converted, but still returned as a -dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer +dictionary. The ``convert_dict_qtables`` method no longer performs any operations on the data given to it, has been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 71ae84c044a..5dd1a61afe1 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -46,7 +46,6 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 -from ._deprecate import deprecate from .JpegPresets import presets # @@ -612,11 +611,6 @@ def _getmp(self): # fmt: on -def convert_dict_qtables(qtables): - deprecate("convert_dict_qtables", 10, action="Conversion is no longer needed") - return qtables - - def get_sampling(im): # There's no subsampling when images have only 1 layer # (grayscale images) or when they are CMYK (4 layers), From 5dbef9e0a8483e1cc680779c49d4e61275920066 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 20:45:51 +0300 Subject: [PATCH 489/727] Remove ImagePalette size parameter, deprecated in 8.4.0 --- Tests/test_imagepalette.py | 4 ---- docs/deprecations.rst | 21 ++++++++++----------- src/PIL/ImagePalette.py | 8 +------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index ac99ef38196..baa698bb4f4 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -9,10 +9,6 @@ def test_sanity(): palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError): - ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10) - def test_reload(): with Image.open("Tests/images/hopper.gif") as im: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index f70a91834b9..e740238b6ae 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,17 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -ImagePalette size parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 8.4.0 - -The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). - -Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by -default, and the size parameter could be used to override that. Pillow 8.3.0 removed -the default required length, also removing the need for the size parameter. - ImageShow.Viewer.show_file file argument ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -209,6 +198,16 @@ JpegImagePlugin.convert_dict_qtables Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer performed any operations on the data given to it, and has been removed. +ImagePalette size parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.4.0 +.. versionremoved:: 10.0.0 + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index e455c04596c..f0c09470863 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -19,7 +19,6 @@ import array from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile -from ._deprecate import deprecate class ImagePalette: @@ -34,16 +33,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None, size=0): + def __init__(self, mode="RGB", palette=None): self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() self.dirty = None - if size != 0: - deprecate("The size parameter", 10, None) - if size != len(self.palette): - msg = "wrong palette size" - raise ValueError(msg) @property def palette(self): From 8d83d5e66a7c73dd01d7dc526afd85f5696026c5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 20:54:59 +0300 Subject: [PATCH 490/727] Remove ImageShow.Viewer.show_file file argument, deprecated in 9.1.0 --- Tests/test_imageshow.py | 17 -------- docs/deprecations.rst | 24 +++++------ src/PIL/ImageShow.py | 89 ++++------------------------------------- 3 files changed, 18 insertions(+), 112 deletions(-) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index eda485cf6de..e54372b60de 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -89,20 +89,3 @@ def test_ipythonviewer(): im = hopper() assert test_viewer.show(im) == 1 - - -@pytest.mark.skipif( - not on_ci() or is_win32(), - reason="Only run on CIs; hangs on Windows CIs", -) -@pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_file_deprecated(tmp_path, viewer): - f = str(tmp_path / "temp.jpg") - hopper().save(f) - with pytest.warns(DeprecationWarning): - try: - viewer.show_file(file=f) - except NotImplementedError: - pass - with pytest.raises(TypeError): - viewer.show_file() diff --git a/docs/deprecations.rst b/docs/deprecations.rst index e740238b6ae..e75d1f7fa66 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,19 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -ImageShow.Viewer.show_file file argument -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.1.0 - -The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been -deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by -``path``. - -In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. -``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest -``viewer.show_file(path="test.jpg")`` instead. - Constants ~~~~~~~~~ @@ -208,6 +195,17 @@ Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular length default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 removed the default required length, also removing the need for the ``size`` parameter. +ImageShow.Viewer.show_file file argument +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +removed and replaced by ``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index f0e73fb9075..3f68a2696bf 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -19,8 +19,6 @@ from PIL import Image -from ._deprecate import deprecate - _viewers = [] @@ -111,21 +109,10 @@ def show_image(self, image, **options): """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) os.system(self.get_command(path, **options)) # nosec return 1 @@ -164,21 +151,10 @@ def get_command(self, file, **options): command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.call(["open", "-a", "Preview.app", path]) executable = sys.executable or shutil.which("python3") if executable: @@ -215,21 +191,10 @@ def get_command_ex(self, file, **options): command = executable = "xdg-open" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["xdg-open", path]) return 1 @@ -246,20 +211,10 @@ def get_command_ex(self, file, title=None, **options): command += f" -title {quote(title)}" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) args = ["display"] title = options.get("title") if title: @@ -278,20 +233,10 @@ def get_command_ex(self, file, **options): command = "gm display" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["gm", "display", path]) return 1 @@ -304,20 +249,10 @@ def get_command_ex(self, file, **options): command = "eog -n" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["eog", "-n", path]) return 1 @@ -336,20 +271,10 @@ def get_command_ex(self, file, title=None, **options): command += f" -name {quote(title)}" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) args = ["xv"] title = options.get("title") if title: From c8ec15980b00261d8c6a105a3dbc2a2fc634940c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 21:19:11 +0300 Subject: [PATCH 491/727] Remove constants deprecated in 9.1.0 --- Tests/test_file_apng.py | 10 ----- Tests/test_file_blp.py | 13 +----- Tests/test_file_ftex.py | 9 ---- Tests/test_image.py | 7 --- Tests/test_imagecms.py | 10 ----- Tests/test_imagefont.py | 9 ---- docs/deprecations.rst | 91 +++++++++++++++++++------------------- src/PIL/BlpImagePlugin.py | 16 ------- src/PIL/FtexImagePlugin.py | 12 ----- src/PIL/Image.py | 16 ------- src/PIL/ImageCms.py | 13 ------ src/PIL/ImageFont.py | 11 ----- src/PIL/PngImagePlugin.py | 12 ----- 13 files changed, 47 insertions(+), 182 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index feca72aa6de..f78c086eb0c 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -655,13 +655,3 @@ def test_different_modes_in_later_frames(mode, tmp_path): im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) with Image.open(test_file) as reloaded: assert reloaded.mode == mode - - -def test_constants_deprecation(): - for enum, prefix in { - PngImagePlugin.Disposal: "APNG_DISPOSE_", - PngImagePlugin.Blend: "APNG_BLEND_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(PngImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index ba2781820e0..8b1355b6280 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,6 +1,6 @@ import pytest -from PIL import BlpImagePlugin, Image +from PIL import Image from .helper import ( assert_image_equal, @@ -72,14 +72,3 @@ def test_crashes(test_file): with Image.open(f) as im: with pytest.raises(OSError): im.load() - - -def test_constants_deprecation(): - for enum, prefix in { - BlpImagePlugin.Format: "BLP_FORMAT_", - BlpImagePlugin.Encoding: "BLP_ENCODING_", - BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(BlpImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index cae20fa46eb..ac6253db056 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -21,12 +21,3 @@ def test_invalid_file(): with pytest.raises(SyntaxError): FtexImagePlugin.FtexImageFile(invalid_file) - - -def test_constants_deprecation(): - for enum, prefix in { - FtexImagePlugin.Format: "FORMAT_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(FtexImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_image.py b/Tests/test_image.py index cb31565e775..85f9f7d0231 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -930,13 +930,6 @@ def test_apply_transparency(self): assert im.palette.colors[(27, 35, 6, 214)] == 24 def test_constants(self): - with pytest.warns(DeprecationWarning): - assert Image.LINEAR == Image.Resampling.BILINEAR - with pytest.warns(DeprecationWarning): - assert Image.CUBIC == Image.Resampling.BICUBIC - with pytest.warns(DeprecationWarning): - assert Image.ANTIALIAS == Image.Resampling.LANCZOS - for enum in ( Image.Transpose, Image.Transform, diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 66be02078ad..8efe063c11d 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -617,16 +617,6 @@ def test_auxiliary_channels_isolated(): assert_image_equal(test_image.convert(dst_format[2]), reference_image) -def test_constants_deprecation(): - for enum, prefix in { - ImageCms.Intent: "INTENT_", - ImageCms.Direction: "DIRECTION_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(ImageCms, prefix + name) == enum[name] - - @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) def test_rgb_lab(mode): im = Image.new(mode, (1, 1)) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index b115517acba..2d83b5a375e 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1130,12 +1130,3 @@ def test_raqm_missing_warning(monkeypatch): "Raqm layout was requested, but Raqm is not available. " "Falling back to basic layout." ) - - -def test_constants_deprecation(): - for enum, prefix in { - ImageFont.Layout: "LAYOUT_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(ImageFont, prefix + name) == enum[name] diff --git a/docs/deprecations.rst b/docs/deprecations.rst index e75d1f7fa66..8606ede4dca 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,51 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Constants -~~~~~~~~~ - -.. deprecated:: 9.1.0 - -A number of constants have been deprecated and will be removed in Pillow 10.0.0 -(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. - -.. note:: - - Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that - was reversed in Pillow 9.4.0 and those constants will now remain available. - See :ref:`restored-image-constants` - -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` -``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` -``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` -``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` -``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` -``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` -``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` -``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` -``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` -``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` -``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` -``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` -``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` -``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` -``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` -``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` -``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` -``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` -``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` -``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` -``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` -===================================================== ============================================================ - FitsStubImagePlugin ~~~~~~~~~~~~~~~~~~~ @@ -206,6 +161,52 @@ removed and replaced by ``path``. In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. +Constants +~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +A number of constants have been removed. +Instead, ``enum.IntEnum`` classes have been added. + +.. note:: + + Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that + was reversed in Pillow 9.4.0 and those constants will now remain available. + See :ref:`restored-image-constants` + +===================================================== ============================================================ +Removed Use instead +===================================================== ============================================================ +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 1cc0d4b3ce9..0ca60ff2471 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -35,7 +35,6 @@ from io import BytesIO from . import Image, ImageFile -from ._deprecate import deprecate class Format(IntEnum): @@ -54,21 +53,6 @@ class AlphaEncoding(IntEnum): DXT5 = 7 -def __getattr__(name): - for enum, prefix in { - Format: "BLP_FORMAT_", - Encoding: "BLP_ENCODING_", - AlphaEncoding: "BLP_ALPHA_ENCODING_", - }.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - def unpack_565(i): return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index c7c32252b87..c46b2f28ba6 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -56,7 +56,6 @@ from io import BytesIO from . import Image, ImageFile -from ._deprecate import deprecate MAGIC = b"FTEX" @@ -66,17 +65,6 @@ class Format(IntEnum): UNCOMPRESSED = 1 -def __getattr__(name): - for enum, prefix in {Format: "FORMAT_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - class FtexImageFile(ImageFile.ImageFile): format = "FTEX" format_description = "Texture File Format (IW2:EOC)" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 99a895fb036..bc846fde759 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -59,22 +59,6 @@ from ._deprecate import deprecate from ._util import DeferredError, is_path - -def __getattr__(name): - old_resampling = { - "LINEAR": "BILINEAR", - "CUBIC": "BICUBIC", - "ANTIALIAS": "LANCZOS", - } - if name in old_resampling: - deprecate( - name, 10, f"{old_resampling[name]} or Resampling.{old_resampling[name]}" - ) - return Resampling[old_resampling[name]] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - logger = logging.getLogger(__name__) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index f87849680df..31b0e5a5ee6 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -20,8 +20,6 @@ from PIL import Image -from ._deprecate import deprecate - try: from PIL import _imagingcms except ImportError as ex: @@ -117,17 +115,6 @@ class Direction(IntEnum): PROOF = 2 -def __getattr__(name): - for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # # flags diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9cdad2961b1..34a04a6bf8d 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -43,17 +43,6 @@ class Layout(IntEnum): RAQM = 1 -def __getattr__(name): - for enum, prefix in {Layout: "LAYOUT_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - try: from . import _imagingft as core except ImportError as ex: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 15a3c8291c4..82a74b26785 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -45,7 +45,6 @@ from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 -from ._deprecate import deprecate logger = logging.getLogger(__name__) @@ -131,17 +130,6 @@ class Blend(IntEnum): """ -def __getattr__(name): - for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - def _safe_zlib_decompress(s): dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) From 575a038f9762ab142948d6d36a4ec3a37b8f7833 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 21:32:18 +0300 Subject: [PATCH 492/727] Remove FitsStubImagePlugin, deprecated in 9.1.0 --- Tests/test_file_fits.py | 38 +---------------- docs/deprecations.rst | 18 ++++---- src/PIL/FitsStubImagePlugin.py | 76 ---------------------------------- src/PIL/__init__.py | 1 - 4 files changed, 10 insertions(+), 123 deletions(-) delete mode 100644 src/PIL/FitsStubImagePlugin.py diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 6f988729f9f..68b3eb567fd 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -2,7 +2,7 @@ import pytest -from PIL import FitsImagePlugin, FitsStubImagePlugin, Image +from PIL import FitsImagePlugin, Image from .helper import assert_image_equal, hopper @@ -48,39 +48,3 @@ def test_comment(): image_data = b"SIMPLE = T / comment string" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) - - -def test_stub_deprecated(): - class Handler: - opened = False - loaded = False - - def open(self, im): - self.opened = True - - def load(self, im): - self.loaded = True - im.fp.close() - return Image.new("RGB", (1, 1)) - - handler = Handler() - with pytest.warns(DeprecationWarning): - FitsStubImagePlugin.register_handler(handler) - - with Image.open(TEST_FILE) as im: - assert im.format == "FITS" - assert im.size == (128, 128) - assert im.mode == "L" - - assert handler.opened - assert not handler.loaded - - im.load() - assert handler.loaded - - FitsStubImagePlugin._handler = None - Image.register_open( - FitsImagePlugin.FitsImageFile.format, - FitsImagePlugin.FitsImageFile, - FitsImagePlugin._accept, - ) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8606ede4dca..577cd6c272f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,15 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -FitsStubImagePlugin -~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.1.0 - -The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in -Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through -:mod:`~PIL.FitsImagePlugin` instead. - FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -207,6 +198,15 @@ Removed Use instead ``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` ===================================================== ============================================================ +FitsStubImagePlugin +~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py deleted file mode 100644 index 50948ec423a..00000000000 --- a/src/PIL/FitsStubImagePlugin.py +++ /dev/null @@ -1,76 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# FITS stub adapter -# -# Copyright (c) 1998-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from . import FitsImagePlugin, Image, ImageFile -from ._deprecate import deprecate - -_handler = None - - -def register_handler(handler): - """ - Install application-specific FITS image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - deprecate( - "FitsStubImagePlugin", - 10, - action="FITS images can now be read without " - "a handler through FitsImagePlugin instead", - ) - - # Override FitsImagePlugin with this handler - # for backwards compatibility - try: - Image.ID.remove(FITSStubImageFile.format) - except ValueError: - pass - - Image.register_open( - FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept - ) - - -class FITSStubImageFile(ImageFile.StubImageFile): - format = FitsImagePlugin.FitsImageFile.format - format_description = FitsImagePlugin.FitsImageFile.format_description - - def _open(self): - offset = self.fp.tell() - - im = FitsImagePlugin.FitsImageFile(self.fp) - self._size = im.size - self.mode = im.mode - self.tile = [] - - self.fp.seek(offset) - - loader = self._load() - if loader: - loader.open(self) - - def _load(self): - return _handler - - -def _save(im, fp, filename): - msg = "FITS save handler not installed" - raise OSError(msg) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_save(FITSStubImageFile.format, _save) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 32d2381f3c2..2bb8f6d7f10 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -31,7 +31,6 @@ "DdsImagePlugin", "EpsImagePlugin", "FitsImagePlugin", - "FitsStubImagePlugin", "FliImagePlugin", "FpxImagePlugin", "FtexImagePlugin", From c9f11565f1be05578f831e8f2050a2164683a70e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 21:40:39 +0300 Subject: [PATCH 493/727] Remove FreeTypeFont.getmask2 fill parameter, deprecated in 9.2.0 --- Tests/test_imagefont.py | 8 -------- docs/deprecations.rst | 17 +++++++++-------- src/PIL/ImageFont.py | 16 +--------------- 3 files changed, 10 insertions(+), 31 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 2d83b5a375e..ca76ca6b297 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1083,14 +1083,6 @@ def test_woff2(layout_engine): assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) -def test_fill_deprecation(font): - with pytest.warns(DeprecationWarning): - font.getmask2("Hello world", fill=Image.core.fill) - with pytest.warns(DeprecationWarning): - with pytest.raises(TypeError): - font.getmask2("Hello world", fill=None) - - def test_render_mono_size(): # issue 4177 diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 577cd6c272f..eaef949074d 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,14 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -FreeTypeFont.getmask2 fill parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been -deprecated and will be removed in Pillow 10 (2023-07-01). - PhotoImage.paste box parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -207,6 +199,15 @@ FitsStubImagePlugin The stub image plugin ``FitsStubImagePlugin`` has been removed. FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. +FreeTypeFont.getmask2 fill parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 34a04a6bf8d..e7d45636c43 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -51,9 +51,6 @@ class Layout(IntEnum): core = DeferredError(ex) -_UNSPECIFIED = object() - - # FIXME: add support for pilfont2 format (see FontFile.py) # -------------------------------------------------------------------- @@ -654,7 +651,6 @@ def getmask2( self, text, mode="", - fill=_UNSPECIFIED, direction=None, features=None, language=None, @@ -680,12 +676,6 @@ def getmask2( .. versionadded:: 1.1.5 - :param fill: Optional fill function. By default, an internal Pillow function - will be used. - - Deprecated. This parameter will be removed in Pillow 10 - (2023-07-01). - :param direction: Direction of the text. It can be 'rtl' (right to left), 'ltr' (left to right) or 'ttb' (top to bottom). Requires libraqm. @@ -738,10 +728,6 @@ def getmask2( :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - if fill is _UNSPECIFIED: - fill = Image.core.fill - else: - deprecate("fill", 10) size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) @@ -750,7 +736,7 @@ def getmask2( size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) offset = offset[0] - stroke_width, offset[1] - stroke_width Image._decompression_bomb_check(size) - im = fill("RGBA" if mode == "RGBA" else "L", size, 0) + im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) if min(size): self.font.render( text, From 584f8c39de3265440b920f3814a9a06ef420dcd7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 21:54:51 +0300 Subject: [PATCH 494/727] Remove PhotoImage.paste() box parameter, deprecated in 9.2.0 --- Tests/test_imagetk.py | 7 ------- docs/deprecations.rst | 15 ++++++++------- src/PIL/ImageTk.py | 9 +-------- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index 995d0ee1f38..a0c9574ba94 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -89,13 +89,6 @@ def test_photoimage_blank(mode): assert_image_equal(reloaded.convert(mode), im) -def test_box_deprecation(): - im = hopper() - im_tk = ImageTk.PhotoImage(im) - with pytest.warns(DeprecationWarning): - im_tk.paste(im, (0, 0, 128, 128)) - - def test_bitmapimage(): im = hopper("1") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index eaef949074d..9b4186d013b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,13 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -PhotoImage.paste box parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). - Image.coerce_e ~~~~~~~~~~~~~~ @@ -208,6 +201,14 @@ FreeTypeFont.getmask2 fill parameter The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been removed. +PhotoImage.paste box parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The ``box`` parameter was unused and has been removed. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index ef569ed2edd..bf98eb2c8c2 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -29,7 +29,6 @@ from io import BytesIO from . import Image -from ._deprecate import deprecate # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -162,7 +161,7 @@ def height(self): """ return self.__size[1] - def paste(self, im, box=None): + def paste(self, im): """ Paste a PIL image into the photo image. Note that this can be very slow if the photo image is displayed. @@ -170,13 +169,7 @@ def paste(self, im, box=None): :param im: A PIL image. The size must match the target region. If the mode does not match, the image is converted to the mode of the bitmap image. - :param box: Deprecated. This parameter will be removed in Pillow 10 - (2023-07-01). """ - - if box is not None: - deprecate("The box parameter", 10, None) - # convert to blittable im.load() image = im.im From b25bf5161a088d17a2ee7ebb509b866683519dad Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 22:16:13 +0300 Subject: [PATCH 495/727] Remove Image.coerce_e, deprecated in 9.2.0 --- Tests/test_image_point.py | 7 ------- docs/deprecations.rst | 16 ++++++++-------- src/PIL/Image.py | 25 ++++++++----------------- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 157ecb120f0..c406cb8ec26 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,7 +1,5 @@ import pytest -from PIL import Image - from .helper import assert_image_equal, hopper @@ -62,8 +60,3 @@ def test_f_mode(): im = hopper("F") with pytest.raises(ValueError): im.point(None) - - -def test_coerce_e_deprecation(): - with pytest.warns(DeprecationWarning): - assert Image.coerce_e(2).data == 2 diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 9b4186d013b..0eb3d6b41df 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,14 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Image.coerce_e -~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -This undocumented method has been deprecated and will be removed in Pillow 10 -(2023-07-01). - .. _Font size and offset methods: Font size and offset methods @@ -222,6 +214,14 @@ Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or `PySide6 `_ instead. +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +This undocumented method has been removed. + PILLOW_VERSION constant ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bc846fde759..0d7a9bbfcbd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -56,7 +56,6 @@ _plugins, ) from ._binary import i32le, o32be, o32le -from ._deprecate import deprecate from ._util import DeferredError, is_path logger = logging.getLogger(__name__) @@ -421,26 +420,18 @@ def _getencoder(mode, encoder_name, args, extra=()): # Simple expression analyzer -def coerce_e(value): - deprecate("coerce_e", 10) - return value if isinstance(value, _E) else _E(1, value) - - -# _E(scale, offset) represents the affine transformation scale * x + offset. -# The "data" field is named for compatibility with the old implementation, -# and should be renamed once coerce_e is removed. class _E: - def __init__(self, scale, data): + def __init__(self, scale, offset): self.scale = scale - self.data = data + self.offset = offset def __neg__(self): - return _E(-self.scale, -self.data) + return _E(-self.scale, -self.offset) def __add__(self, other): if isinstance(other, _E): - return _E(self.scale + other.scale, self.data + other.data) - return _E(self.scale, self.data + other) + return _E(self.scale + other.scale, self.offset + other.offset) + return _E(self.scale, self.offset + other) __radd__ = __add__ @@ -453,19 +444,19 @@ def __rsub__(self, other): def __mul__(self, other): if isinstance(other, _E): return NotImplemented - return _E(self.scale * other, self.data * other) + return _E(self.scale * other, self.offset * other) __rmul__ = __mul__ def __truediv__(self, other): if isinstance(other, _E): return NotImplemented - return _E(self.scale / other, self.data / other) + return _E(self.scale / other, self.offset / other) def _getscaleoffset(expr): a = expr(_E(1, 0)) - return (a.scale, a.data) if isinstance(a, _E) else (0, a) + return (a.scale, a.offset) if isinstance(a, _E) else (0, a) # -------------------------------------------------------------------- From f1f46a718d934fccd924483f147351c0d6a14fa7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 23:41:16 +0300 Subject: [PATCH 496/727] Add removals to 10.0.0 release notes --- docs/releasenotes/10.0.0.rst | 149 +++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + docs/releasenotes/template.rst | 4 +- 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 docs/releasenotes/10.0.0.rst diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst new file mode 100644 index 00000000000..cd0296e3e29 --- /dev/null +++ b/docs/releasenotes/10.0.0.rst @@ -0,0 +1,149 @@ +10.0.0 +------ + +Backwards Incompatible Changes +============================== + +Categories +^^^^^^^^^^ + +``im.category`` has been removed, along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 has been removed. + +JpegImagePlugin.convert_dict_qtables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. + +ImagePalette size parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +removed and replaced by ``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. + +Constants +^^^^^^^^^ + +A number of constants have been removed. +Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Removed Use instead +===================================================== ============================================================ +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. + +FreeTypeFont.getmask2 fill parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + +PhotoImage.paste box parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``box`` parameter was unused and has been removed. + +PyQt5 and PySide2 +^^^^^^^^^^^^^^^^^ + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +^^^^^^^^^^^^^^ + +This undocumented method has been removed. + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 177fb65dd08..9bca9854152 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.0.0 9.5.0 9.4.0 9.3.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index f7271ae2bf8..440d04b1cc4 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -1,5 +1,5 @@ -x.y.z ------ +xx.y.z +------ Backwards Incompatible Changes ============================== From f707e8abd6cc820c62923056b17af9139f6d8b3d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Apr 2023 11:40:31 +1000 Subject: [PATCH 497/727] 1 mode still fails for PyQt6 --- Tests/test_qt_image_toqimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 399df670f61..d071838f729 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -28,7 +28,7 @@ def test_sanity(mode, tmp_path): assert_image_equal(rt, src) if mode == "1": - # BW appears to not save correctly on QT5 + # BW appears to not save correctly on QT # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination # in IHDR From 2cba9904db9df42a1c1502296e567a2d000df3b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Apr 2023 11:48:06 +1000 Subject: [PATCH 498/727] Removed _category --- src/PIL/Image.py | 2 -- src/PIL/MicImagePlugin.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0d7a9bbfcbd..5a43f6c4a43 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -487,7 +487,6 @@ def __init__(self): self._size = (0, 0) self.palette = None self.info = {} - self._category = 0 self.readonly = 0 self.pyaccess = None self._exif = None @@ -604,7 +603,6 @@ def __eq__(self, other): and self.mode == other.mode and self.size == other.size and self.info == other.info - and self._category == other._category and self.getpalette() == other.getpalette() and self.tobytes() == other.tobytes() ) diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 58f7327bde4..801318930d5 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -66,9 +66,6 @@ def _open(self): self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 - if len(self.images) > 1: - self._category = Image.CONTAINER - self.seek(0) def seek(self, frame): From c26b4621597e56c60f54707edde4ca4c88f4cfd7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Apr 2023 11:52:38 +1000 Subject: [PATCH 499/727] Updated documentation --- docs/reference/Image.rst | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 0eba1141a2a..35a4c21107d 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -412,18 +412,6 @@ See :ref:`concept-filters` for details. :undoc-members: :noindex: -Some deprecated filters are also available under the following names: - -.. data:: NONE - :noindex: - :value: Resampling.NEAREST -.. data:: LINEAR - :value: Resampling.BILINEAR -.. data:: CUBIC - :value: Resampling.BICUBIC -.. data:: ANTIALIAS - :value: Resampling.LANCZOS - Dither modes ^^^^^^^^^^^^ From 94aa76ebdeb33be85a6c23d907877b7d45032ba2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 10:29:28 +0300 Subject: [PATCH 500/727] Fix typo --- Tests/test_qt_image_toqimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index d071838f729..95c13ba757b 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -28,7 +28,7 @@ def test_sanity(mode, tmp_path): assert_image_equal(rt, src) if mode == "1": - # BW appears to not save correctly on QT + # BW appears to not save correctly on Qt # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination # in IHDR From aa6c0dcc9e0c3d60ab77ced04f7c0e7e7d1f0fc0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 10:31:47 +0300 Subject: [PATCH 501/727] Cygwin doesn't provide any Qt6 packages Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/test-cygwin.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 397cfe0a187..14a5f2c146b 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -67,7 +67,6 @@ jobs: python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt6-devel-tools wget xorg-server-extra zlib-devel From 4ffbbe194c5a1b8840f809574017ab5f1333695f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Apr 2023 20:14:12 +1000 Subject: [PATCH 502/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 547893d8250..2088a1da5f4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Remove deprecations for Pillow 10.0.0 #7059 + [hugovk, radarhere] + - Drop support for soon-EOL Python 3.7 #7058 [hugovk, radarhere] From 6797e47458177020877bf1efa3bed230e0db6d69 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 19:52:04 +0300 Subject: [PATCH 503/727] Add release check to make sure no TODOs remain in release notes --- Makefile | 1 + Tests/check_release_notes.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 Tests/check_release_notes.py diff --git a/Makefile b/Makefile index f51325d474b..e41f3641108 100644 --- a/Makefile +++ b/Makefile @@ -78,6 +78,7 @@ debug: .PHONY: release-test release-test: + python3 Tests/check_release_notes.py python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py new file mode 100644 index 00000000000..db9ba364b1a --- /dev/null +++ b/Tests/check_release_notes.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +for rst in Path("docs/releasenotes/").rglob("[1-9]*.rst"): + if "TODO" in open(rst).read(): + sys.exit(f"Error: remove TODO from {rst}") From 80a1238e2bf5796145b4d976524a52c603bd53eb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 9 Apr 2023 22:43:36 +0300 Subject: [PATCH 504/727] Simplify glob Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/check_release_notes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py index db9ba364b1a..0a9a898d7f7 100644 --- a/Tests/check_release_notes.py +++ b/Tests/check_release_notes.py @@ -1,6 +1,6 @@ import sys from pathlib import Path -for rst in Path("docs/releasenotes/").rglob("[1-9]*.rst"): +for rst in Path("docs/releasenotes").glob("[1-9]*.rst"): if "TODO" in open(rst).read(): sys.exit(f"Error: remove TODO from {rst}") From dd15f15d08bf3fd32c41ef9f2502286778c9f993 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 09:06:20 +1000 Subject: [PATCH 505/727] Added further field sizes --- Tests/test_file_tiff.py | 9 ++++++++- src/PIL/TiffImagePlugin.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 97a02ac969f..43181d1b32c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -96,10 +96,17 @@ def test_mac_tiff(self): assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self): + def test_bigtiff(self, tmp_path): with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + # multistrip support not yet implemented + del im.tag_v2[273] + + outfile = str(tmp_path / "temp.tif") + im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910abd..5b7d3f3020b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1892,6 +1892,10 @@ class AppendingTiffWriter: 8, # srational 4, # float 8, # double + 4, # ifd + 2, # unicode + 4, # complex + 8, # long8 ] # StripOffsets = 273 From b2301d70d104f36a08ae658f569d02f7796fc8fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Apr 2023 19:10:51 +1000 Subject: [PATCH 506/727] Removed ImageFont.getsize and related functions --- Tests/images/imagedraw_stroke_multiline.png | Bin 4061 -> 4059 bytes Tests/test_deprecate.py | 19 +- Tests/test_font_pcf.py | 3 - Tests/test_imagedraw.py | 19 +- Tests/test_imagedraw2.py | 16 -- Tests/test_imagefont.py | 95 +--------- docs/deprecations.rst | 98 +++++----- docs/releasenotes/10.0.0.rst | 16 ++ docs/releasenotes/9.2.0.rst | 20 +- src/PIL/ImageDraw.py | 97 +--------- src/PIL/ImageDraw2.py | 16 -- src/PIL/ImageFont.py | 193 -------------------- src/PIL/_deprecate.py | 2 - 13 files changed, 96 insertions(+), 498 deletions(-) diff --git a/Tests/images/imagedraw_stroke_multiline.png b/Tests/images/imagedraw_stroke_multiline.png index fc5e07c8679d5aa12a1d95152ea6d2542f169697..c290fc0568e5c4b40299a208cde7d0d760077fd0 100644 GIT binary patch delta 3993 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 delta 3990 zcmV;H4{7k*AKf32B!8qyL_t(|ob8=?e3aF>$G_PLO9%-hECRz?L@~C2fEE|HYT3lf z4$@pH+G45RUPZYsZQUwZ+unYtTwJ&xipnO6vg2hDRAg181_TAdmW7=FSx7R^{bSzA zcqS8)nR%CFIN#6td?xeE`=0ZhU*7kfXU_Y)=YU3|(P%UpjgzJi9e<zi|`ek z1~I9Wzz2}%bk|I#4P=|Akze_f)4em9B_Q@yw802&xy+nswK?=l@dYTrwX1JsB?{hpa=GW{H@lXsxc3bn0tm7?-DT882 zak`tPu@HoeU3`3}nSa+TPx;v@5crMLeKd_$a3B;V77z2ib4StcAP7tDRkm6%3Iw)0 z-A6NN?2WU|)KsY^XDCka88+T({552$vSCp@lFx#`^-lNE8D0fp(o98^3A2sAV#@r# zSbzOLAn>ZwZaY8>d)lO5 zAg~z*1^84d0Bj75Dq=2hh9t5HPT1ANIPk_K8?=~GlXn27O4Fd&Slbv+m zUOoe1?s!Fou76pTu16a|m^~p-z_r6M-|eM?amZY3;FDl`4-m*#zv(hTxaG5}IpBH@ zgv>dL1e0fgz+crZ>q7wud%Ikn0ap|p1Y!LhzRq8cqv=Ty$f&ga8z4L}T@h>RO(2jL z@d4LuAhbD*JIdGjCHI2B*MYY04MOu=N@EqhuHEBgz<(9?cvcO`1Q`D|QR1`xGt=`D zX!|X&hrH&j8ladXZiK5c-fPgl&c0*hR4Q?op$yWi*y}M6hJIK@z?Eui4{T%{2z@py z-JUTS1XfhNR~lR2;lB5k3b*x)fPm{d5R%U-H|Rh8 zDF_@5HC#!DQsOR?pQh+#4B^`na9I;)Wde*|KthSnW`tMofIFl*^I-+0alCWCVm1P< z2STdTd7|OC{|~ytL>iy-gx+D)d=Cg+vwxH$E2l-B4!G_Hq0@e)OY=v2e$<6mb1dY7 znkOMJa0LiFVvhUvG7p3opH})~T29(@5FUD6seI805ctvr-|Zy{3W;)axJ{)qMNBJ6 z=eblrP{4JAwVgBj0zU#0x&0H`9P!!S9Y?vT>DdKIEFSKAn^3}-`9PifUA+l}Ie(cZ z9!51PM!=O{hXSs4;HJSv|C|nciV<+#V`2YHJq>>WQ4D*Zh8NAOwT9HS5v~IutnKf> zVI>t`BbTA6(>hd7Ul3X!p*-3dv7{~qTyKH!)Jux@cdWNoo++;=hB^{(CBk_SzWtHU z^`qAiU2d&d&2y_5;j%d33Xd@$^nco-^u@@3TxeON--57Wpwj5WuYpub_^Ms324UtT z#qVyrL984+`7aPgE>s!~oJv@WXaQv;6mvSo=k%@&5I6~OmbDuwF+kwDZ$MzE`3;&8 zXWb7m3|U6X8K2Dz&qjA zT|Gs{(+0+vwU=6XCdR=D5Wc$6_h0or;?>a>A8rAm)iLFlLvaEro*5=GZ1JH+3eBWx! zQP@CHB8${?-r97T-v+{hQOZB_rmY~=Rc0r{qvXIM@(?DQGkUm@wLi1cDhbf%0Y%mCX6`U6}2p!S@uKy;;?p1+NPg;<> zr2%yOUddsy-;ElGl>@M|d(aLA#Hc4H$lY=PzU@L`eMjA>o@c2bq~|ejOjy^f?6$l- z)te#GCwMo$0pZS%+0rYlYgS%5^Tv>#2Lg|qE>=DG#Ek1iMG0-r!yd-am1*VwAi-3j z4C3Gs*bI3}oE6NW1&3i(c=s>bz)aW!7eW=t4SDc6Os=(eG?Aenf95JAD(F38;FmBC zIzn?RCuep;5fnl$9Dyv@37+3Qcp2=Jh{!S zop#0@#ijb>r}5>FS#mFb_#*>WQvZ_o;}Uo?nAxlyU@bT?msmw9s>jkH4E>PUQme?AF; zl6Y?T8U$W4sYR=BSud7{Mlj2Ggy`Yd{Po(_)4TFR3sW048JAsRc^C{Q3AVg*)A6FXArvW_Wyotn+*a#F}XFn zmKn8(XCa4wbz^-$#eDY~0Qv>mB@F7gSj8|O;Dh@Va}Uq4Z1#%NbzCfrg-W;gRBS)m z2be$GS=|@w2*8mxip`kUva!ic)qSzf1C*#kTJ?%Oe=jCCRrkee1=E zx-V8|fX@3B+X>H9Ag4`ks_u(54xrz9#kRvU@Q`J4Q*~Udb}*2baz-yu%%AoJ;B%8( ztIJ}=!v8`XznF_F+h;!7nhkeA34CI5gLPJ{Xm|^5r)3VWJfVCt>(`XW!258fihZ5O zW#ezde`Ht+Lc!cpLcN$`g4?uLRZz>#HjS=Z0If^Y#0b6SBQaM>!>g@%lt z|AY6BI^F%?aM^uiWp!op43f@!-*14P)p}V=a5-=q3S4Z!-yfmb2@r6@T=U2LEWqWA ze^{PU(1>SVput5DI26*VK-M#k?f+YSj^@>R-A>|C|`K0I5 zc|X@>al>x9?jcG7?{%;}W9PB87nH&8;2)thZ40gdN0zx+G@jD*B*PZ-&bYu+yOzfC z)JsHp<_DePv?f*Js#>fN4z74;1kK=Df4B|C!t?MR9P!njSjdBKD2oJEl-U!!8I(_& z-cM6-nJU&5=l~DHCL=c_x}1k!<7~3OUUSD2QwDBpm$VHPmtA62&+ zujhUb0;htXB;0abwu|MVtFiP*lQZn@rnFubL*Gpx@J}lp2nClzVgdDF2?#B6e>mP! zao|9EVoRaioZRm^JFNUeieVJ2C$|No7a%_fhuR-x%zTKhZA$>VoIA2Sq{4Bil%}86 z6$H*(v>ZxkxSSRXxDSL@$G9A)xY1=VhzWy&5pfz;@B!@Mcq^6W8gZURhL>oDTPS_KuifA77t^epV*3b*cv6YD5Iwo9=!I|)E?xOGRISf>Gs z5*3?~kQDY~i4iAO8bE_fip^N9=ThaZgCpX^Y6Z~tuwpxt0+^877I9+T3Gm~u6x;sJ z02~On?uZj>IKaS_itS6!(*t|Ltvj;Bx(_l)JG3|!9rkZuUeKuA5scO4Z6 zPBx{XXJIk3A9@N8mqTJ@7%PxAIm5P|{v*r3ngs$MRbP$0xNH}zJ-lw@ZDs5VaGPgc zKcd`F0@qsOK2r*?$o)KWJ)}z;WSA2ZNq}D?r1|EWqaNO>x z3j9iZ2@~J+zest?=@c?{fxyQQV_MgPOlFIPr7*y>=7S=sfMzFYc9QNp>A#LUHjsQS zz-FhK^0U=^?YWkXhH|_8^kUq;$S=0_UCW^ z!SMG;KI{GL0!)J>r}a^&Zb~Qz%9rq%O{tN=XZ}UQFJU}%gmimAAI1AaZ559nn waL7gnG#ZUYqtR$Ik*ODxum}VhG*>` for more information. - - Returns width and height (in pixels) of given text. - - :param text: Text to measure. - - :return: (width, height) - """ - deprecate("getsize", 10, "getbbox or getlength") - return self.font.getsize(text) - def getmask(self, text, mode="", *args, **kwargs): """ Create a bitmap for the text. @@ -398,165 +380,6 @@ def getbbox( width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width return left, top, left + width, top + height - def getsize( - self, - text, - direction=None, - features=None, - language=None, - stroke_width=0, - ): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`getlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text if rendered in font with - provided direction, features, and language. - - .. note:: For historical reasons this function measures text height from - the ascender line instead of the top, see :ref:`text-anchors`. - If you wish to measure text height from the top, it is recommended - to use the bottom value of :meth:`getbbox` with ``anchor='lt'`` instead. - - :param text: Text to measure. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - """ - deprecate("getsize", 10, "getbbox or getlength") - # vertical offset is added for historical reasons - # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 - size, offset = self.font.getsize(text, "L", direction, features, language) - return ( - size[0] + stroke_width * 2, - size[1] + stroke_width * 2 + offset[1], - ) - - def getsize_multiline( - self, - text, - direction=None, - spacing=4, - features=None, - language=None, - stroke_width=0, - ): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.ImageDraw.multiline_textbbox` instead. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text if rendered in font - with provided direction, features, and language, while respecting - newline characters. - - :param text: Text to measure. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param spacing: The vertical gap between lines, defaulting to 4 pixels. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - """ - deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox") - max_width = 0 - lines = self._multiline_split(text) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing - for line in lines: - line_width, line_height = self.getsize( - line, direction, features, language, stroke_width - ) - max_width = max(max_width, line_width) - - return max_width, len(lines) * line_spacing - spacing - - def getoffset(self, text): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` instead. - - See :ref:`deprecations ` for more information. - - Returns the offset of given text. This is the gap between the - starting coordinate and the first marking. Note that this gap is - included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. - - :param text: Text to measure. - - :return: A tuple of the x and y offset - """ - deprecate("getoffset", 10, "getbbox") - return self.font.getsize(text)[1] - def getmask( self, text, @@ -851,22 +674,6 @@ def __init__(self, font, orientation=None): self.font = font self.orientation = orientation # any 'transpose' argument, or None - def getsize(self, text, *args, **kwargs): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. - - See :ref:`deprecations ` for more information. - """ - deprecate("getsize", 10, "getbbox or getlength") - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w, h = self.font.getsize(text) - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - return h, w - return w, h - def getmask(self, text, mode="", *args, **kwargs): im = self.font.getmask(text, mode, *args, **kwargs) if self.orientation is not None: diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 81f2189dcfc..2f2a3df13e3 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -45,8 +45,6 @@ def deprecate( elif when <= int(__version__.split(".")[0]): msg = f"{deprecated} {is_} deprecated and should be removed." raise RuntimeError(msg) - elif when == 10: - removed = "Pillow 10 (2023-07-01)" elif when == 11: removed = "Pillow 11 (2024-10-15)" else: From adbb04d5dca5ae4f59ce51ca7474a3918e1f377d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 5 Apr 2023 12:32:13 +0300 Subject: [PATCH 507/727] Formatting for readability --- Tests/test_imagefont.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f43044efe8f..a614b0fe59f 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -306,7 +306,10 @@ def test_rotated_transposed_font(font, orientation): bbox_b = draw.textbbox((20, 20), word) # Check (w,h) of box a is (h,w) of box b - assert (bbox_a[2] - bbox_a[0], bbox_a[3] - bbox_a[1]) == ( + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( bbox_b[3] - bbox_b[1], bbox_b[2] - bbox_b[0], ) @@ -349,7 +352,10 @@ def test_unrotated_transposed_font(font, orientation): length_b = draw.textlength(word) # Check boxes a and b are same size - assert (bbox_a[2] - bbox_a[0], bbox_a[3] - bbox_a[1]) == ( + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( bbox_b[2] - bbox_b[0], bbox_b[3] - bbox_b[1], ) From b27794fc01c84b876fe876e8ebbd8b4b6a4f78ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 17:24:16 +1000 Subject: [PATCH 508/727] Added test for ImageDraw2 textbbox --- Tests/test_imagedraw2.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 2a5219893ac..a8a2ee1fce6 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -2,7 +2,7 @@ import pytest -from PIL import Image, ImageDraw, ImageDraw2 +from PIL import Image, ImageDraw, ImageDraw2, features from .helper import ( assert_image_equal, @@ -170,6 +170,21 @@ def test_text(): assert_image_similar_tofile(im, expected, 13) +@skip_unless_feature("freetype2") +def test_textbbox(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + bbox = draw.textbbox((0, 0), "ImageDraw2", font) + + # Assert + right = 72 if features.check_feature("raqm") else 70 + assert bbox == (0, 2, right, 12) + + @skip_unless_feature("freetype2") def test_textsize_empty_string(): # Arrange From fa6cd4a19519c7c6af06265e39dc5f0e51fdd734 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 17:34:33 +1000 Subject: [PATCH 509/727] Only check width and height of transposed fonts once --- Tests/test_imagefont.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index a614b0fe59f..623365d53fe 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -305,7 +305,7 @@ def test_rotated_transposed_font(font, orientation): draw.font = transposed_font bbox_b = draw.textbbox((20, 20), word) - # Check (w,h) of box a is (h,w) of box b + # Check (w, h) of box a is (h, w) of box b assert ( bbox_a[2] - bbox_a[0], bbox_a[3] - bbox_a[1], @@ -314,11 +314,8 @@ def test_rotated_transposed_font(font, orientation): bbox_b[2] - bbox_b[0], ) - # Check bbox b is (20, 20, 20 + h, 20 + w) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] - assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) # text length is undefined for vertical text with pytest.raises(ValueError): @@ -360,11 +357,8 @@ def test_unrotated_transposed_font(font, orientation): bbox_b[3] - bbox_b[1], ) - # Check bbox b is (20, 20, 20 + w, 20 + h) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] - assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) assert length_a == length_b From bc0bf5efea357a18c8f10a0c5768e837891b808e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 18:47:58 +1000 Subject: [PATCH 510/727] Preserve line spacing backwards compatibility --- Tests/images/imagedraw_stroke_multiline.png | Bin 4059 -> 4061 bytes Tests/test_imagedraw.py | 4 ++-- src/PIL/ImageDraw.py | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Tests/images/imagedraw_stroke_multiline.png b/Tests/images/imagedraw_stroke_multiline.png index c290fc0568e5c4b40299a208cde7d0d760077fd0..fc5e07c8679d5aa12a1d95152ea6d2542f169697 100644 GIT binary patch delta 3990 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 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 17ee75dfa5f..7ffd7969d90 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1220,8 +1220,8 @@ def test_textbbox_stroke(): # Act / Assert assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20) assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22) - assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 42) - assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 46) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) @skip_unless_feature("freetype2") diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c16ff28cc8e..e9ccf80413f 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -431,7 +431,11 @@ def _multiline_split(self, text): return text.split(split_character) def _multiline_spacing(self, font, spacing, stroke_width): - return self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + spacing + return ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) def text( self, From fe4e52deacf1be96bb69758739d7fc88967a12e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Apr 2023 16:15:42 +1000 Subject: [PATCH 511/727] Rearranged code --- setup.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index d780b038ade..453ea688f4e 100755 --- a/setup.py +++ b/setup.py @@ -473,17 +473,15 @@ def build_extensions(self): lib_root = include_root = root if lib_root is not None: - if isinstance(lib_root, str): - _add_directory(library_dirs, lib_root) - else: - for lib_dir in lib_root: - _add_directory(library_dirs, lib_dir) + if not isinstance(lib_root, (tuple, list)): + lib_root = (lib_root,) + for lib_dir in lib_root: + _add_directory(library_dirs, lib_dir) if include_root is not None: - if isinstance(include_root, str): - _add_directory(include_dirs, include_root) - else: - for include_dir in include_root: - _add_directory(include_dirs, include_dir) + if not isinstance(include_root, (tuple, list)): + include_root = (include_root,) + for include_dir in include_root: + _add_directory(include_dirs, include_dir) # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): From 16aa710c7804c24b8518d50927de92aac7cf5b1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 20:14:15 +1000 Subject: [PATCH 512/727] Updated documentation --- docs/deprecations.rst | 2 - docs/reference/ImageDraw.rst | 110 ----------------------------------- 2 files changed, 112 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 64d0569c86b..45b2f42000f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,8 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -.. _Font size and offset methods: - PSFile ~~~~~~ diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 43a5a2bc2b3..aec7a3ef89f 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -474,116 +474,6 @@ Methods .. versionadded:: 8.0.0 -.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) - - .. deprecated:: 9.2.0 - - See :ref:`deprecations ` for more information. - - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - - Return the size of the given string, in pixels. - - .. note:: For historical reasons this function measures text height from - the ascender line instead of the top, see :ref:`text-anchors`. - If you wish to measure text height from the top, it is recommended - to use :meth:`textbbox` with ``anchor='lt'`` instead. - - :param text: Text to be measured. If it contains any newline characters, - the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: If the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`, - the number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - -.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) - - .. deprecated:: 9.2.0 - - See :ref:`deprecations ` for more information. - - Use :py:meth:`.multiline_textbbox` instead. - - Return the size of the given string, in pixels. - - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - - .. note:: For historical reasons this function measures text height as the - distance between the top ascender line and bottom descender line, - not the top and bottom of the text, see :ref:`text-anchors`. - If you wish to measure text height from the top to the bottom of text, - it is recommended to use :meth:`multiline_textbbox` instead. - - :param text: Text to be measured. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: The number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - .. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) Returns length (in pixels with 1/64 precision) of given text when rendered From 5ef3ddafe30bfd64ff49f52f05c7a97712655a73 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 20:56:46 +1000 Subject: [PATCH 513/727] Do not install PyQt6-Qt6 6.5.0 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index 6aa122cc56e..81631a9bd77 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -43,7 +43,7 @@ if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 - python3 -m pip install pyqt6 + python3 -m pip install pyqt6 PyQt6-Qt6!=6.5.0 fi # webp From 900db50a74fd75a8a108e236a7940527da49149b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 17:16:02 +0300 Subject: [PATCH 514/727] Move 'git push --all' later --- RELEASING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index c203a9c1265..2f28372ac17 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -18,7 +18,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. ```bash git branch 5.2.x git tag 5.2.0 - git push --all git push --tags ``` * [ ] Create and check source distribution: @@ -32,8 +31,11 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. python3 -m twine upload dist/Pillow-5.2.0* ``` * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` - +* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), + increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: + ```bash + git push --all + ``` ## Point Release Released as needed for security, installation or critical bug fixes. From 34908960f0a8f8b70756a232e1e754a987d6d634 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 17:11:20 +0300 Subject: [PATCH 515/727] Flip order to give slower macOS/Linux a headstart --- RELEASING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 2f28372ac17..feb6f9469a0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -97,11 +97,7 @@ Released as needed privately to individual vendors for critical security-related ## Binary Distributions -### Windows -* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/` - -### Mac and Linux +### macOS and Linux * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): ```bash git clone https://github.com/python-pillow/pillow-wheels @@ -111,6 +107,10 @@ Released as needed privately to individual vendors for critical security-related * [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) and copy into `dist/` +### Windows +* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) + and copy into `dist/` + ## Publicize Release * [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 From c9ec517b4d2bd44cf7905d8fcb7e37c01c21541c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 18:56:37 +0300 Subject: [PATCH 516/727] Give examples how to download binary releases using gh CLI --- RELEASING.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index feb6f9469a0..48f7ea1046c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -105,11 +105,18 @@ Released as needed privately to individual vendors for critical security-related ./update-pillow-tag.sh [[release tag]] ``` * [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) - and copy into `dist/` + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): + ```bash + gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels + ``` ### Windows * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/` + and copy into `dist/`For example using [GitHub CLI](https://github.com/cli/cli): + ```bash + gh run download --dir dist + # select dist-x.y.z + ``` ## Publicize Release From 47ac51cd8fda3ef701926fbffe01eb914e936eff Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 18:59:44 +0300 Subject: [PATCH 517/727] Update PEP links to redirects --- RELEASING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 48f7ea1046c..97f0144f87d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -11,7 +11,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Develop and prepare release in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions. -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: @@ -31,7 +31,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. python3 -m twine upload dist/Pillow-5.2.0* ``` * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: ```bash git push --all @@ -51,7 +51,7 @@ Released as needed for security, installation or critical bug fixes. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: ```bash From 6a3c3991395825b564779854f83afa9a285dd79c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 9 Apr 2023 22:39:24 +0300 Subject: [PATCH 518/727] Fix typo Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index 97f0144f87d..ac9187ac95f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -112,7 +112,7 @@ Released as needed privately to individual vendors for critical security-related ### Windows * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/`For example using [GitHub CLI](https://github.com/cli/cli): + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): ```bash gh run download --dir dist # select dist-x.y.z From 08b553957b0d4a8cad189534d6cf1a0dc8adc866 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Apr 2023 22:09:03 +1000 Subject: [PATCH 519/727] Move "git push" commands later --- RELEASING.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index ac9187ac95f..bb0d4e03867 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -47,16 +47,12 @@ Released as needed for security, installation or critical bug fixes. git checkout -t remotes/origin/5.2.x ``` * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. - - - * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: ```bash git tag 5.2.1 - git push git push --tags ``` * [ ] Create and check source distribution: @@ -69,7 +65,10 @@ Released as needed for security, installation or critical bug fixes. python3 -m twine check --strict dist/* python3 -m twine upload dist/Pillow-5.2.1* ``` -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: + ```bash + git push + ``` ## Embargoed Release @@ -85,7 +84,6 @@ Released as needed privately to individual vendors for critical security-related ```bash git checkout 2.5.x git tag 2.5.3 - git push origin 2.5.x git push origin --tags ``` * [ ] Create and check source distribution: @@ -93,7 +91,10 @@ Released as needed privately to individual vendors for critical security-related make sdist ``` * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: + ```bash + git push origin 2.5.x + ``` ## Binary Distributions From cb68187006e02b3aeda44160338b67891455749e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 10 Apr 2023 15:45:40 +0300 Subject: [PATCH 520/727] Clarify command should be run from main repo --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index bb0d4e03867..604bb1b8c38 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -106,7 +106,7 @@ Released as needed privately to individual vendors for critical security-related ./update-pillow-tag.sh [[release tag]] ``` * [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) - and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo: ```bash gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels ``` From 38c2449ef98f5634463fc06691bffd3c1213a527 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Apr 2023 07:31:43 +1000 Subject: [PATCH 521/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2088a1da5f4..f38cb5c0ed7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- -- Remove deprecations for Pillow 10.0.0 #7059 +- Remove deprecations for Pillow 10.0.0 #7059, #7080 [hugovk, radarhere] - Drop support for soon-EOL Python 3.7 #7058 From 9aefa8d5eb41c78330d240f37f1cf175ac5379b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Apr 2023 12:20:27 +1000 Subject: [PATCH 522/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f38cb5c0ed7..cd0b95085cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed type handling for include and lib directories #7069 + [adisbladis, radarhere] + - Remove deprecations for Pillow 10.0.0 #7059, #7080 [hugovk, radarhere] From 6fac6b15f2cf67a9d9d118e10e5bdd8622014f26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Apr 2023 21:13:13 +1000 Subject: [PATCH 523/727] Revert "Do not install PyQt6-Qt6 6.5.0" This reverts commit 5ef3ddafe30bfd64ff49f52f05c7a97712655a73. --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index 81631a9bd77..6aa122cc56e 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -43,7 +43,7 @@ if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 - python3 -m pip install pyqt6 PyQt6-Qt6!=6.5.0 + python3 -m pip install pyqt6 fi # webp From 8aa61243e5300c4407ace4f016f448a8e9d648aa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Apr 2023 21:13:49 +1000 Subject: [PATCH 524/727] Install libxcb-cursor0 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index 6aa122cc56e..17c349ab1f4 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -42,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi From 2d216d3d3d030a52750a22694eeab1eb92acc844 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 12 Apr 2023 18:27:57 +0300 Subject: [PATCH 525/727] Don't install unused and deleted codecov --- .ci/after_success.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/after_success.sh b/.ci/after_success.sh index 23a6fcd4d45..c71546f007b 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -1,7 +1,7 @@ #!/bin/bash # gather the coverage data -python3 -m pip install codecov +python3 -m pip install coverage if [[ $MATRIX_DOCKER ]]; then python3 -m coverage xml --ignore-errors else From 3bb6344541a14f45ed1bfac3b0f7ff090be08423 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 12 Apr 2023 18:12:21 +0300 Subject: [PATCH 526/727] Replace deleted codecov package with bash uploader --- .appveyor.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 9ed192e0f37..47cbb55a807 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -52,8 +52,9 @@ test_script: #- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? after_test: -- python -m pip install codecov -- codecov --file coverage.xml --name %PYTHON% --flags AppVeyor +- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe +- chmod +x codecov +- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor matrix: fast_finish: true From f619675115275a330b0d99660c4442fe7f7d3fe2 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 12 Apr 2023 21:14:38 +0200 Subject: [PATCH 527/727] Update vendored Raqm to 0.10.1 --- src/thirdparty/raqm/COPYING | 2 +- src/thirdparty/raqm/NEWS | 35 ++++++++++++++++++++++++++++++ src/thirdparty/raqm/README.md | 2 +- src/thirdparty/raqm/raqm-version.h | 4 ++-- src/thirdparty/raqm/raqm.c | 25 ++++++++++++++++----- src/thirdparty/raqm/raqm.h | 2 +- 6 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING index c605a5dc67a..97e2489b779 100644 --- a/src/thirdparty/raqm/COPYING +++ b/src/thirdparty/raqm/COPYING @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016-2022 Khaled Hosny +Copyright © 2016-2023 Khaled Hosny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index ae1128485f2..e8bf32e0bbb 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -1,3 +1,38 @@ +Overview of changes leading to 0.10.1 +Wednesday, April 12, 2023 +==================================== + +Make combining marks always inherit the script of their base. + +Overview of changes leading to 0.10.0 +Wednesday, January 11, 2023 +==================================== + +Fix font feature ranges. + +Fix resolved direction for all-neutral text. + +Implement letter and word spacing support. + +New API: + * raqm_set_text_utf16 + +Overview of changes leading to 0.9.0 +Sunday, January 30, 2022 +==================================== + +Raise the minimum versions of Raqm dependencies: no longer conditionally +enabling any features based on specific dependency version. + +raqm_t objects can now be reused by calling raqm_clear_contents() before +re-use, to potentially reduce the number memory allocations. + +Don't hardcode python3 in tests. + +New API: + * raqm_set_freetype_load_flags_range + * raqm_clear_contents + Overview of changes leading to 0.8.0 Monday, December 13, 2021 ==================================== diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md index 315e0c8d822..ab729cdc036 100644 --- a/src/thirdparty/raqm/README.md +++ b/src/thirdparty/raqm/README.md @@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm: [1]: https://github.com/fribidi/fribidi [2]: https://github.com/Tehreer/SheenBidi [3]: https://github.com/harfbuzz/harfbuzz -[4]: https://www.freetype.org +[4]: https://freetype.org/ [5]: https://www.gtk.org/gtk-doc diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index bdb6fb66264..62d2d206459 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -33,9 +33,9 @@ #define RAQM_VERSION_MAJOR 0 #define RAQM_VERSION_MINOR 10 -#define RAQM_VERSION_MICRO 0 +#define RAQM_VERSION_MICRO 1 -#define RAQM_VERSION_STRING "0.10.0" +#define RAQM_VERSION_STRING "0.10.1" #define RAQM_VERSION_ATLEAST(major,minor,micro) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 770ea30182b..2b331e1afb0 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2022 Khaled Hosny + * Copyright © 2016-2023 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -1432,7 +1432,7 @@ raqm_get_glyphs (raqm_t *rq, * * Since: 0.8 */ -RAQM_API raqm_direction_t +raqm_direction_t raqm_get_par_resolved_direction (raqm_t *rq) { if (!rq) @@ -1455,7 +1455,7 @@ raqm_get_par_resolved_direction (raqm_t *rq) * * Since: 0.8 */ -RAQM_API raqm_direction_t +raqm_direction_t raqm_get_direction_at_index (raqm_t *rq, size_t index) { @@ -2021,6 +2021,22 @@ _get_pair_index (const uint32_t ch) #define STACK_IS_EMPTY(script) ((script)->size <= 0) #define IS_OPEN(pair_index) (((pair_index) & 1) == 0) +static hb_script_t +_raqm_unicode_script (hb_codepoint_t u) +{ + static hb_unicode_funcs_t* unicode_funcs; + + unicode_funcs = hb_unicode_funcs_get_default (); + + /* Make combining marks inherit the script of their bases, regardless of + * their own script. + */ + if (hb_unicode_general_category (unicode_funcs, u) == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) + return HB_SCRIPT_INHERITED; + + return hb_unicode_script (unicode_funcs, u); +} + /* Resolve the script for each character in the input string, if the character * script is common or inherited it takes the script of the character before it * except paired characters which we try to make them use the same script. We @@ -2033,10 +2049,9 @@ _raqm_resolve_scripts (raqm_t *rq) int last_set_index = -1; hb_script_t last_script = HB_SCRIPT_INVALID; _raqm_stack_t *stack = NULL; - hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default (); for (size_t i = 0; i < rq->text_len; ++i) - rq->text_info[i].script = hb_unicode_script (unicode_funcs, rq->text[i]); + rq->text_info[i].script = _raqm_unicode_script (rq->text[i]); #ifdef RAQM_TESTING RAQM_TEST ("Before script detection:\n"); diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h index 2fd836c8607..6fd6089c70d 100644 --- a/src/thirdparty/raqm/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2022 Khaled Hosny + * Copyright © 2016-2023 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to From 087a5f889a2e0dd350f7f586b357207e1055356b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 13 Apr 2023 07:00:07 +0300 Subject: [PATCH 528/727] Remove chmod Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 47cbb55a807..b2599e3d889 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -53,7 +53,6 @@ test_script: after_test: - curl -Os https://uploader.codecov.io/latest/windows/codecov.exe -- chmod +x codecov - .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor matrix: From 86716ff2b094bb0b071208bdf7299d5eefa95085 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 13 Apr 2023 07:52:32 +1000 Subject: [PATCH 529/727] Updated raqm to 0.10.1 --- depends/install_raqm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index d1b31cfa53b..24c1f9c3029 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.10.0 +archive=libraqm-0.10.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz From accbd8ad93203d7c6068c75885e22f81e7164c98 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 14 Apr 2023 11:13:28 +1000 Subject: [PATCH 530/727] Updated nasm to 2.16.01 --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b2599e3d889..36f5bd0ad68 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -26,9 +26,9 @@ install: - 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ +- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\ - choco install ghostscript --version=10.0.0.20230317 -- path c:\nasm-2.15.05;C:\Program Files\gs\gs10.00.0\bin;%PATH% +- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 8f4d53ecf06..a008801114f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -71,8 +71,8 @@ jobs: - name: Install dependencies id: install run: | - 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" - echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH + 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" + echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH choco install ghostscript --version=10.0.0.20230317 echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH From 73e94824888e6ed8019cc36099c92eea9b45e340 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 09:41:19 +1000 Subject: [PATCH 531/727] Select Python version --- .github/workflows/test-cygwin.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 14a5f2c146b..6a5f738575f 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -84,6 +84,10 @@ jobs: restore-keys: | ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- + - name: Select Python version + run: | + ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + - name: Build system information run: | dash.exe -c "python3 .github/workflows/system-info.py" From 8d3014b8bf94ee6d07276b227dfac4d6f7f5a859 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 21:03:59 +1000 Subject: [PATCH 532/727] Added inPlace argument to exif_transpose() --- Tests/images/orientation_rectangle.jpg | Bin 0 -> 669 bytes Tests/test_imageops.py | 12 ++++++ src/PIL/ImageOps.py | 53 +++++++++++++++---------- 3 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 Tests/images/orientation_rectangle.jpg diff --git a/Tests/images/orientation_rectangle.jpg b/Tests/images/orientation_rectangle.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85cfbd0a813a1b36d64318ead4f0fe5a3258a51f GIT binary patch literal 669 zcmex=wh=DOELf4NWZ*Q!{f5ODks= zS2uSLPp{yR(6I1`$f)F$)U@=B%&g*)(z5c3%Btp;*0%PJ&aO$5r%atTea6gLixw|g zx@`H1m8&*w-m-Pu_8mKS9XfpE=&|D`PM*4S`O4L6*Kgds_3+W-Cr_U}fAR9w$4{TX zeEs(Q$Io9Ne=#yJL%anfAwEO%mmttzOe`$SEbJhEF*22dJTAz>s%Xe2([0-9])", ): - transposed_image.info["XML:com.adobe.xmp"] = re.sub( - pattern, "", transposed_image.info["XML:com.adobe.xmp"] + exif_image.info["XML:com.adobe.xmp"] = re.sub( + pattern, "", exif_image.info["XML:com.adobe.xmp"] ) + if inPlace: + return return transposed_image + if inPlace: + return return image.copy() From 099d696dc7d3c349265ae3cdbb5f949bca1e2866 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Sat, 15 Apr 2023 18:24:19 +0800 Subject: [PATCH 533/727] Fix ImageGrab with wl-paste --- Tests/test_imagegrab.py | 19 +++++++++++++++++++ src/PIL/ImageGrab.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index fa88065f43c..e7c2c6c9fdf 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -98,3 +98,22 @@ def test_grabclipboard_png(self): im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, "Tests/images/hopper.png") + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ["wl-paste", "wl-copy"]) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize( + "image_path", ["Tests/images/hopper.gif", "Tests/images/hopper.png"] + ) + def test_grabclipboard_wl_clipboard(self, image_path): + with open(image_path, mode="rb") as raw_image: + try: + subprocess.call(["wl-copy"], stdin=raw_image) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) + except OSError as e: + pytest.skip(str(e)) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f206d..175eb467199 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -135,6 +135,12 @@ def grabclipboard(): else: if shutil.which("wl-paste"): args = ["wl-paste"] + output = subprocess.check_output(["wl-paste", "-l"]).decode() + mime_types = output.splitlines() + for image_type in ["image/gif", "image/png"]: + if image_type in mime_types: + args.extend(["-t", image_type]) + break elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: From bcb8dfc2fa9011c99d6332d5b0aa00c82e1c6cdf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 22:30:18 +1000 Subject: [PATCH 534/727] Rearranged code --- src/PIL/ImageOps.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 460a21ce2b8..5dc34f4424c 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -622,9 +622,7 @@ def exif_transpose(image, inPlace=False): exif_image.info["XML:com.adobe.xmp"] = re.sub( pattern, "", exif_image.info["XML:com.adobe.xmp"] ) - if inPlace: - return - return transposed_image - if inPlace: - return - return image.copy() + if not inPlace: + return transposed_image + elif not inPlace: + return image.copy() From 6acb381656626305fb2695ef955cfd8bec858570 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 17:23:08 +1000 Subject: [PATCH 535/727] Simplified NumPy install command --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 6a5f738575f..2b597a94580 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -99,7 +99,7 @@ jobs: - name: Install a different NumPy shell: dash.exe -l "{0}" run: | - python3 -m pip install -U 'numpy!=1.21.*' + python3 -m pip install -U numpy - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" From fe8599c5d64b2932b430a0178f17009855397fe6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 16 Apr 2023 14:04:39 +1000 Subject: [PATCH 536/727] Use ExifTags --- src/PIL/Image.py | 4 ++-- src/PIL/ImageOps.py | 8 ++++---- src/PIL/TiffImagePlugin.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 5a43f6c4a43..34b8bbcbd71 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1418,12 +1418,12 @@ def getexif(self): self._exif.load(exif_info) # XMP tags - if 0x0112 not in self._exif: + if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: - self._exif[0x0112] = int(match[2]) + self._exif[ExifTags.Base.Orientation] = int(match[2]) return self._exif diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 5dc34f4424c..facc30ba0ee 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,7 +21,7 @@ import operator import re -from . import Image, ImagePalette +from . import ExifTags, Image, ImagePalette # # helpers @@ -589,7 +589,7 @@ def exif_transpose(image, inPlace=False): image will be returned. """ image_exif = image.getexif() - orientation = image_exif.get(0x0112) + orientation = image_exif.get(ExifTags.Base.Orientation) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -608,8 +608,8 @@ def exif_transpose(image, inPlace=False): exif_image = image if inPlace else transposed_image exif = exif_image.getexif() - if 0x0112 in exif: - del exif[0x0112] + if ExifTags.Base.Orientation in exif: + del exif[ExifTags.Base.Orientation] if "exif" in exif_image.info: exif_image.info["exif"] = exif.tobytes() elif "Raw profile type exif" in exif_image.info: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910abd..7f8449ea673 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -49,7 +49,7 @@ from fractions import Fraction from numbers import Number, Rational -from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags +from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 @@ -1183,7 +1183,7 @@ def get_photoshop_blocks(self): :returns: Photoshop "Image Resource Blocks" in a dictionary. """ blocks = {} - val = self.tag_v2.get(0x8649) + val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: while val[:4] == b"8BIM": id = i16(val[4:6]) @@ -1548,7 +1548,7 @@ def _setup(self): palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) - self._tile_orientation = self.tag_v2.get(0x0112) + self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation) # From 6d12581385688c3af964e6707a1b4ed2651d31a5 Mon Sep 17 00:00:00 2001 From: Carl Weaver Date: Sun, 16 Apr 2023 15:37:38 +0800 Subject: [PATCH 537/727] Update src/PIL/ImageGrab.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 175eb467199..6550a770684 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -136,10 +136,11 @@ def grabclipboard(): if shutil.which("wl-paste"): args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() - mime_types = output.splitlines() - for image_type in ["image/gif", "image/png"]: - if image_type in mime_types: - args.extend(["-t", image_type]) + clipboard_mimetypes = output.splitlines() + Image.preinit() + for mimetype in Image.MIME.values(): + if mimetype in clipboard_mimetypes: + args.extend(["-t", mimetype]) break elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] From 3d54b8e2b2419255a6b5a74dd0f2841ea4de7416 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Sun, 16 Apr 2023 15:41:14 +0800 Subject: [PATCH 538/727] Remove useless try catch block --- Tests/test_imagegrab.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index e7c2c6c9fdf..703472c4a1e 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -111,9 +111,6 @@ def test_grabclipboard_png(self): ) def test_grabclipboard_wl_clipboard(self, image_path): with open(image_path, mode="rb") as raw_image: - try: - subprocess.call(["wl-copy"], stdin=raw_image) - im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, image_path) - except OSError as e: - pytest.skip(str(e)) + subprocess.call(["wl-copy"], stdin=raw_image) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) From 57bbe6df2c25a1a5155f4c25ae0bdf2f2769d035 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 15:59:12 +1000 Subject: [PATCH 539/727] Remove use of deprecated "bpp" member --- src/libImaging/Jpeg2KEncode.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 8f637006160..0d7e896b7f8 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -281,7 +281,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { int ret = -1; unsigned prec = 8; - unsigned bpp = 8; unsigned _overflow_scale_factor; stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE); @@ -313,7 +312,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_i16; prec = 16; - bpp = 12; } else if (strcmp(im->mode, "LA") == 0) { components = 2; color_space = OPJ_CLRSPC_GRAY; @@ -342,7 +340,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { image_params[n].h = im->ysize; image_params[n].x0 = image_params[n].y0 = 0; image_params[n].prec = prec; - image_params[n].bpp = bpp; image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; } From cc84ff5e7d721a1309ba8e6ebd80238f90af389d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Apr 2023 16:10:29 +1000 Subject: [PATCH 540/727] Note that open() seeks to the start of file objects --- src/PIL/Image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 5a43f6c4a43..6dfde70a0f4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3156,7 +3156,8 @@ def open(fp, mode="r", formats=None): :param fp: A filename (string), pathlib.Path object or a file object. The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, - and be opened in binary mode. + and be opened in binary mode. The file object will also seek to zero + before reading. :param mode: The mode. If given, this argument must be "r". :param formats: A list or tuple of formats to attempt to load the file in. This can be used to restrict the set of formats checked. From aa2e662995eaf67e2f9f53d6817a173c12b45a19 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Mon, 17 Apr 2023 16:44:43 +0800 Subject: [PATCH 541/727] Add sway and wl-clipboard dependencies to GitHub CI workflow --- .ci/install.sh | 3 ++- .github/workflows/test.yml | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 17c349ab1f4..d5cbd82488b 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -22,7 +22,8 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ - cmake meson imagemagick libharfbuzz-dev libfribidi-dev + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard fi python3 -m pip install --upgrade pip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fced6113b50..53b7ee68833 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,7 +84,15 @@ jobs: python3 -m pip install pytest-reverse fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh + export XDG_RUNTIME_DIR="/tmp/headless-sway" + export SWAYSOCK="$XDG_RUNTIME_DIR/sway.sock" + export WLR_BACKENDS=headless + export WLR_LIBINPUT_NO_DEVICES=1 + mkdir "$XDG_RUNTIME_DIR" + xvfb-run -s '-screen 0 1024x768x24'\ + sway -V -d -c /dev/null& + export WAYLAND_DISPLAY=wayland-1 + .ci/test.sh else .ci/test.sh fi From 4e6f1f1ac60cb9e88c0605739468693e0e17e4e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Apr 2023 19:08:59 +1000 Subject: [PATCH 542/727] Removed Fedora 36 --- .github/workflows/test-docker.yml | 1 - docs/installation.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 14592ea1d53..cbe6c2ca3d8 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,7 +39,6 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-x86, - fedora-36-amd64, fedora-37-amd64, gentoo, ubuntu-18.04-bionic-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index 7088657f905..0ac5914dabc 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -448,8 +448,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ -| Fedora 36 | 3.10 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | From 6ffa189d0156f52df422cd99fa3d28ebb0432107 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 09:16:01 +0000 Subject: [PATCH 543/727] Update cygwin/cygwin-install-action action to v4 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 2b597a94580..9a1e46705de 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v3 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v3 + uses: cygwin/cygwin-install-action@v4 with: platform: x86_64 packages: > From b7585b0597855f15ccf998d84684d90488a1133a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Apr 2023 10:27:36 +1000 Subject: [PATCH 544/727] Removed unnecessary settings --- .github/workflows/test.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53b7ee68833..afb8fb56c77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,13 +84,7 @@ jobs: python3 -m pip install pytest-reverse fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - export XDG_RUNTIME_DIR="/tmp/headless-sway" - export SWAYSOCK="$XDG_RUNTIME_DIR/sway.sock" - export WLR_BACKENDS=headless - export WLR_LIBINPUT_NO_DEVICES=1 - mkdir "$XDG_RUNTIME_DIR" - xvfb-run -s '-screen 0 1024x768x24'\ - sway -V -d -c /dev/null& + xvfb-run -s '-screen 0 1024x768x24' sway& export WAYLAND_DISPLAY=wayland-1 .ci/test.sh else From f15d7265f779c04d4e01b425f1e8b7211422a7dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Apr 2023 10:33:31 +1000 Subject: [PATCH 545/727] Call init() if mimetype is not found with preinit() --- Tests/test_imagegrab.py | 11 +++++------ src/PIL/ImageGrab.py | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 703472c4a1e..065c9c1b583 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -106,11 +106,10 @@ def test_grabclipboard_png(self): ), reason="Linux with wl-clipboard only", ) - @pytest.mark.parametrize( - "image_path", ["Tests/images/hopper.gif", "Tests/images/hopper.png"] - ) - def test_grabclipboard_wl_clipboard(self, image_path): - with open(image_path, mode="rb") as raw_image: - subprocess.call(["wl-copy"], stdin=raw_image) + @pytest.mark.parametrize("ext", ("gif", "png", "ico")) + def test_grabclipboard_wl_clipboard(self, ext): + image_path = "Tests/images/hopper." + ext + with open(image_path, "rb") as fp: + subprocess.call(["wl-copy"], stdin=fp) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, image_path) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 6550a770684..55b50fb4840 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -137,11 +137,19 @@ def grabclipboard(): args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() clipboard_mimetypes = output.splitlines() + + def find_mimetype(): + for mime in Image.MIME.values(): + if mime in clipboard_mimetypes: + return mime + Image.preinit() - for mimetype in Image.MIME.values(): - if mimetype in clipboard_mimetypes: - args.extend(["-t", mimetype]) - break + mimetype = find_mimetype() + if not mimetype: + Image.init() + mimetype = find_mimetype() + if mimetype: + args.extend(["-t", mimetype]) elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: From 75c4acf02c6128f1fe03a14c202a6bcf57eaa31f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 19 Apr 2023 06:46:58 -0600 Subject: [PATCH 546/727] Fix typo --- docs/reference/Image.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 35a4c21107d..41d3b8fcec0 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -439,7 +439,7 @@ Used to specify the dithering method to use for the Palettes ^^^^^^^^ -Used to specify the pallete to use for the :meth:`~Image.convert` method. +Used to specify the palette to use for the :meth:`~Image.convert` method. .. autoclass:: Palette :members: From d2256338b82730fef647a3cd8b65bc9040a7d73e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Apr 2023 23:15:20 +1000 Subject: [PATCH 547/727] Use later value for duplicate xref entries --- Tests/images/duplicate_xref_entry.pdf | Bin 0 -> 3326 bytes Tests/test_pdfparser.py | 6 ++++++ src/PIL/PdfParser.py | 9 +++------ 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 Tests/images/duplicate_xref_entry.pdf diff --git a/Tests/images/duplicate_xref_entry.pdf b/Tests/images/duplicate_xref_entry.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f57a57d61c6ad0649448af316f689db4e7f1322e GIT binary patch literal 3326 zcmeH}S#Z-v7{?{cr{WAa2{dFvvpL)(9cpQ1`C?K!#KsU>hQvt=rPBu=n?xDMN@V3? zrVr4YH+r{EK<~cMmR?V#rRN(xfxeX9?GyB-54hh-K4N>ADRh|WbXZv$>)UU4zfY_G z_mg)x7QYVrWZsL?8cFITgHlUqSjlG91%yQ(Ju+loMBs-qnleu`UPdBPQ&R&2yfC&j zLy!dCA+!3)F536e(v=uhw)HjrEf+<1Cht5G`bra+-sVeSG25c>$rMtTYEd|@%5sv zb=~dleWCt!B9>*jqc?JWSQ_y8WrcSs#sUif`UBv~_gs z=392`ymePPL&hg2m8rZwH@~pBwENDx?!M>V`|jWOz=IDx{K%t^J^sX@C!c!ynP;DS z{)HD`dilsJufF#B8*jaR?45Vtd;fzEKl=ESPe1$o#K}{qzxw){Z@>HghaZ1B`|~fq z{`UJHf1dk`=EX9cnHF5l#A@>LKwcKBm9si%UaVySPR?Vsbz*zd#t}zywz*5%<7^q+ zfAH{8SGPZLW>rc%&adu~PkYbO)QrsjSz>!HDYJ57mApuq>@K|!@nqas$i;ijjE_d?$oguT~S4{zapYpI=RtG zD^+gN(?~Fbi>YXYuTMN!nR-movjp{>FEBbom^`ERL#(ubDQYeTWeoMj)=Q$~7iCGr zQb3eLyTD_cnz>+Sxn3=5WSkdKh&SV;R}>5c{6RF1N;VvTSd5-r*%pz*wKj~!dOtA$ z8(1?|j6`Z}7)O?k)wQIOh1yHEIiadLqD43Xl~CDDAXH%}H?AW3e2hKK>q$*F^1xCg z!h5}&w#U7fQ0ei1>{Y|1x3;JROyAZjw$z}gZa^JVs$e9QVPs^UK(M3LObXR5c!fz9 zM%5K2RkQYg%p@w5f6XK+u8Uz3)J$3f&5zJ3Ce`YmdR`_d;bp+P{8Yjv0*Rtjp^^mO zz6Mh01;_vmQ(DPLs@Q?kB|H6Nwu&tXpTnjN$OAh;8!FQchh?a>)ix>UuOpchia?32 z_QN4*8Oe-J5r;x%Jj>Sz1nc~yaCq4obrduJP{JUV=_e4GM+WpvBpSO@!$c_(4wy{* E8-kd> ]" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" + + +def test_duplicate_xref_entry(): + pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") + assert pdf.xref_table.existing_entries[6][0] == 1197 + pdf.close() diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 1b3cb52a2dc..dc1012f54d3 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -957,14 +957,11 @@ def read_xref_table(self, xref_section_offset): check_format_condition(m, "xref entry not found") offset = m.end() is_free = m.group(3) == b"f" - generation = int(m.group(2)) if not is_free: + generation = int(m.group(2)) new_entry = (int(m.group(1)), generation) - check_format_condition( - i not in self.xref_table or self.xref_table[i] == new_entry, - "xref entry duplicated (and not identical)", - ) - self.xref_table[i] = new_entry + if i not in self.xref_table: + self.xref_table[i] = new_entry return offset def read_indirect(self, ref, max_nesting=-1): From 895f5a4ffc70fafaae2d16e6618c373e1689186d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Apr 2023 09:04:46 +1000 Subject: [PATCH 548/727] Updated macOS tested Pillow versions [ci skip] --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 0ac5914dabc..8798c079169 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -490,7 +490,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |arm | +| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ From b10379b3c14eba9d32a5e81d5242c7c5857a32cc Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Fri, 21 Apr 2023 17:42:45 +0300 Subject: [PATCH 549/727] Load image before deepcopy(__getstate__) Signed-off-by: bigcat88 --- Tests/test_numpy.py | 9 +++++++++ src/PIL/Image.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 147f94a71fd..6dc53d1e81f 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,4 +1,5 @@ import warnings +from copy import deepcopy import pytest @@ -226,6 +227,14 @@ def test_load_first(): assert a.shape == (88, 590) +@skip_unless_feature("libtiff") +def test_load_first_deepcopy(): + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im_deepcopy = deepcopy(im) + a = numpy.array(im_deepcopy) + assert a.shape == (88, 590) + + def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 5a43f6c4a43..bee9e23d088 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -672,7 +672,8 @@ def __array_interface__(self): return new def __getstate__(self): - return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] + im_data = self.tobytes() # load image first + return [self.info, self.mode, self.size, self.getpalette(), im_data] def __setstate__(self, state): Image.__init__(self) From 91b69857c78cb75bb096306a6460c27e70a55c38 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 10:13:56 +1000 Subject: [PATCH 550/727] Removed duplicate code --- src/PIL/ImageCms.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 31b0e5a5ee6..38cbab19ce7 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -185,12 +185,8 @@ def __init__(self, profile): def _set(self, profile, filename=None): self.profile = profile self.filename = filename - if profile: - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info - else: - self.product_name = None - self.product_info = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info def tobytes(self): """ From b62287da3a529bc8e47ab330a81dfd1d8959cef8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 11:18:56 +1000 Subject: [PATCH 551/727] Moved test to test_image_copy --- Tests/test_image_copy.py | 9 ++++++++- Tests/test_numpy.py | 9 --------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 591832147d7..cd602fc76f6 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -4,7 +4,7 @@ from PIL import Image -from .helper import hopper +from .helper import hopper, skip_unless_feature @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) @@ -42,3 +42,10 @@ def test_copy_zero(): out = im.copy() assert out.mode == im.mode assert out.size == im.size + + +@skip_unless_feature("libtiff") +def test_deepcopy(): + with Image.open("Tests/images/g4_orientation_5.tif") as im: + out = copy.deepcopy(im) + assert out.size == (590, 88) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6dc53d1e81f..147f94a71fd 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,5 +1,4 @@ import warnings -from copy import deepcopy import pytest @@ -227,14 +226,6 @@ def test_load_first(): assert a.shape == (88, 590) -@skip_unless_feature("libtiff") -def test_load_first_deepcopy(): - with Image.open("Tests/images/g4_orientation_5.tif") as im: - im_deepcopy = deepcopy(im) - a = numpy.array(im_deepcopy) - assert a.shape == (88, 590) - - def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) From 81a756e93b102b20a8c4599d4b5b3235109ef5ab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 13:45:18 +1000 Subject: [PATCH 552/727] Support float font sizes --- Tests/test_imagefont.py | 10 ++++++++++ src/_imagingft.c | 14 +++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 623365d53fe..7ea485a55ec 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -191,6 +191,16 @@ def test_getlength( assert length == length_raqm +def test_float_size(): + lengths = [] + for size in (48, 48.5, 49): + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine + ) + lengths.append(f.getlength("text")) + assert lengths[0] != lengths[1] != lengths[2] + + def test_render_multiline(font): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) diff --git a/src/_imagingft.c b/src/_imagingft.c index 19785a47f69..78e3f7f104f 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -116,7 +116,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { int error = 0; char *filename = NULL; - Py_ssize_t size; + float size; + FT_Size_RequestRec req; + FT_Long width; Py_ssize_t index = 0; Py_ssize_t layout_engine = 0; unsigned char *encoding; @@ -133,7 +135,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { if (!PyArg_ParseTupleAndKeywords( args, kw, - "etn|nsy#n", + "etf|nsy#n", kwlist, Py_FileSystemDefaultEncoding, &filename, @@ -179,7 +181,13 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { } if (!error) { - error = FT_Set_Pixel_Sizes(self->face, 0, size); + width = size * 64; + req.type = FT_SIZE_REQUEST_TYPE_NOMINAL; + req.width = width; + req.height = width; + req.horiResolution = 0; + req.vertResolution = 0; + error = FT_Request_Size(self->face, &req); } if (!error && encoding && strlen((char *)encoding) == 4) { From d0b41da094c48077c3511988e98c168bd45f84dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 21:22:01 +1000 Subject: [PATCH 553/727] Support I mode for BuiltinFilter --- Tests/images/hopper_emboss_I.png | Bin 0 -> 13273 bytes Tests/images/hopper_emboss_more_I.png | Bin 0 -> 14624 bytes Tests/test_image_filter.py | 43 +++++---- src/libImaging/Filter.c | 133 ++++++++++++++++++-------- 4 files changed, 120 insertions(+), 56 deletions(-) create mode 100644 Tests/images/hopper_emboss_I.png create mode 100644 Tests/images/hopper_emboss_more_I.png diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png new file mode 100644 index 0000000000000000000000000000000000000000..f4dab388fe5c2dce3771133525df2c09b0fada45 GIT binary patch literal 13273 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 WC}ig2^aR-e0000= pow(2, 31) - 1) { + return pow(2, 31) - 1; + } + return (INT32)in; +} + Imaging ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { Imaging imOut; @@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[2]) + (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ + _i2f(in0[x + d]) * (kernel)[2]) int x = 0, y = 0; @@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 1; y < im->ysize - 1; y++) { + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *out = (INT32 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip32(ss); + } + out[x] = in0[x]; + } + } else { + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + out[x] = in0[x]; } - out[x] = in0[x]; } } else { // Add one time for rounding @@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { void ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x5(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ - _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[3] + \ - _i2f((UINT8)in0[x + d + d]) * (kernel)[4]) + (_i2f(in0[x - d - d]) * (kernel)[0] + \ + _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \ + _i2f(in0[x + d]) * (kernel)[3] + \ + _i2f(in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; @@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 2; y < im->ysize - 2; y++) { + INT32 *in_2 = (INT32 *)im->image[y - 2]; + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *in2 = (INT32 *)im->image[y + 2]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip32(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } else { + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; } } else { // Add one time for rounding @@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (!im || im->type != IMAGING_TYPE_UINT8) { + if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { return (Imaging)ImagingError_ModeError(); } From f5c1f7a2c27c810d3d95dc659c5c24c455ca6f74 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 21:47:36 +1000 Subject: [PATCH 554/727] Added Fedora 38 --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index cbe6c2ca3d8..84c46b1d4fb 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -40,6 +40,7 @@ jobs: centos-stream-9-amd64, debian-11-bullseye-x86, fedora-37-amd64, + fedora-38-amd64, gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index 8798c079169..21dcd02274f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -450,6 +450,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 38 | 3.11 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | From e3cb4bb8e00fcaf4c3e0783f7c02e51372595659 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Apr 2023 07:48:53 +1000 Subject: [PATCH 555/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cd0b95085cd..b82333af7a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Load before getting size in __getstate__ #7105 + [bigcat88, radarhere] + - Fixed type handling for include and lib directories #7069 [adisbladis, radarhere] From ab3d0c071e60da904062d22f6b9a73e8fc6cdcb9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Apr 2023 21:03:27 +1000 Subject: [PATCH 556/727] Raise error from stderr of Linux grabclipboard command --- src/PIL/ImageGrab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f206d..2592ba2df3d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -141,8 +141,11 @@ def grabclipboard(): msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) fh, filepath = tempfile.mkstemp() - subprocess.call(args, stdout=fh) + err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr os.close(fh) + if err: + msg = f"{args[0]} error: {err.strip().decode()}" + raise ChildProcessError(msg) im = Image.open(filepath) im.load() os.unlink(filepath) From 99a474a9e63d5bf2ea2654bb8102731db90ff8c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Apr 2023 23:55:29 +1000 Subject: [PATCH 557/727] Removed Ubuntu 18.04 --- .github/workflows/test-docker.yml | 1 - docs/installation.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 84c46b1d4fb..4f01abe44c2 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -42,7 +42,6 @@ jobs: fedora-37-amd64, fedora-38-amd64, gentoo, - ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, ] diff --git a/docs/installation.rst b/docs/installation.rst index 21dcd02274f..a254ec8c2ad 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -457,8 +457,6 @@ These platforms are built and tested for every change. | macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | From d0e9a13c0c8ee0c77559e604c8758720991d930a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 26 Apr 2023 17:08:06 +1000 Subject: [PATCH 558/727] Build all readthedocs formats --- .readthedocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 98d9e4425ac..ec3300dd1f2 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,7 @@ version: 2 +formats: all + build: os: ubuntu-22.04 tools: From 0c8db130afe5343358b30fadffa6e59d2c877083 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 27 Apr 2023 13:31:14 +1000 Subject: [PATCH 559/727] Updated harfbuzz to 7.2.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3f639454b08..9b5fc5d1840 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -337,9 +337,9 @@ def cmd_msbuild( "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.1.0.zip", - "filename": "harfbuzz-7.1.0.zip", - "dir": "harfbuzz-7.1.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.2.0.zip", + "filename": "harfbuzz-7.2.0.zip", + "dir": "harfbuzz-7.2.0", "license": "COPYING", "build": [ *cmds_cmake( From ebd3c47425fe5535f8d1c8eab15bbf1e52899939 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Apr 2023 15:02:11 +1000 Subject: [PATCH 560/727] When saving, allow alpha differences to indicate different frames --- Tests/test_file_apng.py | 14 ++++++++++++++ src/PIL/PngImagePlugin.py | 4 ++-- src/_imaging.c | 12 +++++++++--- src/libImaging/GetBBox.c | 7 ++++--- src/libImaging/Imaging.h | 2 +- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f78c086eb0c..cf1312b770e 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -374,6 +374,20 @@ def test_apng_save(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) +def test_apng_save_alpha(tmp_path): + test_file = str(tmp_path / "temp.png") + + im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) + im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) + im.save(test_file, save_all=True, append_images=[im2]) + + with Image.open(test_file) as reloaded: + assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255) + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) + + def test_apng_save_split_fdat(tmp_path): # test to make sure we do not generate sequence errors when writing # frames with image data spanning multiple fdAT chunks (in this case diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 82a74b26785..7afd6a2a6e8 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1138,9 +1138,9 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) else: base_im = previous["im"] delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") + im_frame.convert("RGBA"), base_im.convert("RGBA") ) - bbox = delta.getbbox() + bbox = delta.im.getbbox(False) if ( not bbox and prev_disposal == encoderinfo.get("disposal") diff --git a/src/_imaging.c b/src/_imaging.c index 281f3a4d2e6..62e51da26e2 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2160,9 +2160,15 @@ _isblock(ImagingObject *self) { } static PyObject * -_getbbox(ImagingObject *self) { +_getbbox(ImagingObject *self, PyObject *args) { int bbox[4]; - if (!ImagingGetBBox(self->image, bbox)) { + + int consider_alpha = 1; + if (!PyArg_ParseTuple(args, "|i", &consider_alpha)) { + return NULL; + } + + if (!ImagingGetBBox(self->image, bbox, consider_alpha)) { Py_INCREF(Py_None); return Py_None; } @@ -3574,7 +3580,7 @@ static struct PyMethodDef methods[] = { {"isblock", (PyCFunction)_isblock, METH_NOARGS}, - {"getbbox", (PyCFunction)_getbbox, METH_NOARGS}, + {"getbbox", (PyCFunction)_getbbox, METH_VARARGS}, {"getcolors", (PyCFunction)_getcolors, METH_VARARGS}, {"getextrema", (PyCFunction)_getextrema, METH_NOARGS}, {"getprojection", (PyCFunction)_getprojection, METH_NOARGS}, diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index e73153600d0..c1570cd3e5a 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -19,7 +19,7 @@ #include "Imaging.h" int -ImagingGetBBox(Imaging im, int bbox[4]) { +ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha) { /* Get the bounding box for any non-zero data in the image.*/ int x, y; @@ -58,10 +58,11 @@ ImagingGetBBox(Imaging im, int bbox[4]) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if ( + } else if (consider_alpha && ( strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || - strcmp(im->mode, "PA") == 0) { + strcmp(im->mode, "PA") == 0 + )) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index d9ded185238..2563a0c6271 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -317,7 +317,7 @@ ImagingMerge(const char *mode, Imaging bands[4]); extern int ImagingSplit(Imaging im, Imaging bands[4]); extern int -ImagingGetBBox(Imaging im, int bbox[4]); +ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha); typedef struct { int x, y; INT32 count; From 96bdbc4afe8ea9c6f113c2676368b2a11bc9fefb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Apr 2023 19:11:02 +1000 Subject: [PATCH 561/727] Renamed variable --- src/_imaging.c | 6 +++--- src/libImaging/GetBBox.c | 4 ++-- src/libImaging/Imaging.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 62e51da26e2..87f5b67055d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2163,12 +2163,12 @@ static PyObject * _getbbox(ImagingObject *self, PyObject *args) { int bbox[4]; - int consider_alpha = 1; - if (!PyArg_ParseTuple(args, "|i", &consider_alpha)) { + int alpha_only = 1; + if (!PyArg_ParseTuple(args, "|i", &alpha_only)) { return NULL; } - if (!ImagingGetBBox(self->image, bbox, consider_alpha)) { + if (!ImagingGetBBox(self->image, bbox, alpha_only)) { Py_INCREF(Py_None); return Py_None; } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index c1570cd3e5a..86c687ca0a8 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -19,7 +19,7 @@ #include "Imaging.h" int -ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha) { +ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { /* Get the bounding box for any non-zero data in the image.*/ int x, y; @@ -58,7 +58,7 @@ ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if (consider_alpha && ( + } else if (alpha_only && ( strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || strcmp(im->mode, "PA") == 0 diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 2563a0c6271..42420887d30 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -317,7 +317,7 @@ ImagingMerge(const char *mode, Imaging bands[4]); extern int ImagingSplit(Imaging im, Imaging bands[4]); extern int -ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha); +ImagingGetBBox(Imaging im, int bbox[4], int alpha_only); typedef struct { int x, y; INT32 count; From b62c3baeeedb642b8eaf4dc16245fb54bcb92dfd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Apr 2023 06:32:27 +1000 Subject: [PATCH 562/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b82333af7a1..7b13900a8df 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Support float font sizes #7107 + [radarhere] + +- Use later value for duplicate xref entries in PdfParser #7102 + [radarhere] + - Load before getting size in __getstate__ #7105 [bigcat88, radarhere] From ff003bfbcc9cfd7d281030f836616c0cdd59cfa6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Apr 2023 14:49:40 +1000 Subject: [PATCH 563/727] Added unpacker from I;16B to I;16 --- Tests/test_lib_pack.py | 1 + src/libImaging/Unpack.c | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index de3e7d1569b..f7812f62bd8 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -757,6 +757,7 @@ def test_F_float(self): def test_I16(self): self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 7eeadf944ea..a0fa22c7d3e 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { } } static void +unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out[1] = in[0]; + in += 2; + out += 2; + } +} +static void unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { int i; for (i = 0; i < pixels; i++) { @@ -1764,6 +1774,7 @@ static struct { {"I;16L", "I;16L", 16, copy2}, {"I;16N", "I;16N", 16, copy2}, + {"I;16", "I;16B", 16, unpackI16B_I16}, {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16B", "I;16N", 16, unpackI16N_I16B}, From b7d19e83d00b08c643c69bca6ea3f6e603c72a98 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 19:15:29 +0000 Subject: [PATCH 564/727] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/tox-dev/tox-ini-fmt: 1.0.0 → 1.3.0](https://github.com/tox-dev/tox-ini-fmt/compare/1.0.0...1.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3b6dc0a6ad..4882a317faa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.0.0 + rev: 1.3.0 hooks: - id: tox-ini-fmt From a766fa4cd1c749ca27375079ef92a57dc538c905 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 19:16:00 +0000 Subject: [PATCH 565/727] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tox.ini | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index d7948ef6dfb..458a0010734 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] -minversion = 1.9 -envlist = +requires = + tox>=4.2 +env_list = lint py{py3, 311, 310, 39, 38} @@ -23,7 +24,7 @@ skip_install = true deps = check-manifest pre-commit -passenv = +pass_env = PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure From 15ef533df9847e556eca0eaa50b7738bb71b8c34 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 May 2023 08:41:18 +1000 Subject: [PATCH 566/727] Added alpha_only argument to getbbox() --- Tests/test_image_getbbox.py | 15 +++++++++++++++ src/PIL/Image.py | 7 +++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index af69ed57a60..eec97821098 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import hopper @@ -38,3 +40,16 @@ def check(im, fill_color): for color in ((0, 0), (127, 0), (255, 0)): im = Image.new(mode, (100, 100), color) check(im, (255, 255)) + + +@pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) +def test_bbox_alpha_only_false(mode): + im = Image.new(mode, (100, 100)) + assert im.getbbox(False) is None + + fill_color = [1] * Image.getmodebands(mode) + fill_color[-1] = 0 + im.paste(tuple(fill_color), (25, 25, 75, 75)) + assert im.getbbox(False) == (25, 25, 75, 75) + + assert im.getbbox() is None diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bee9e23d088..f5d1206717d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1279,11 +1279,14 @@ def getbands(self): """ return ImageMode.getmode(self.mode).bands - def getbbox(self): + def getbbox(self, alpha_only=True): """ Calculates the bounding box of the non-zero regions in the image. + :param alpha_only: Optional flag, defaulting to true. + If true and the image has an alpha channel, trim transparent pixels. + Otherwise, trim pixels when all channels are zero. :returns: The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. See :ref:`coordinate-system`. If the image is completely empty, this @@ -1292,7 +1295,7 @@ def getbbox(self): """ self.load() - return self.im.getbbox() + return self.im.getbbox(alpha_only) def getcolors(self, maxcolors=256): """ From d9921f697aed24bd21b7c090bb6edd55acc8997f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 May 2023 08:29:20 +1000 Subject: [PATCH 567/727] Use stdlib for setuptools on MinGW --- .github/workflows/test-mingw.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ddfafc9d7f4..a109ec0d8b5 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -80,7 +80,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . - name: Test Pillow run: | From db7326674e5261e19fa35f3a32efaf6dea48a5cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 May 2023 13:03:31 +1000 Subject: [PATCH 568/727] Updated libimagequant to 4.2.0 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 362ad95a2db..fd6000ee12b 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.1.1 +archive=libimagequant-4.2.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index a254ec8c2ad..ad27b67eeaf 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -181,7 +181,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.1.1** + * Pillow has been tested with libimagequant **2.6-4.2** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 3fc446c2770b091b07d3b6cff63e87385c19e7fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 May 2023 22:54:18 +1000 Subject: [PATCH 569/727] Added width argument to regular_polygon --- Tests/images/imagedraw_triangle_width.png | Bin 0 -> 499 bytes Tests/test_imagedraw.py | 20 ++++++++++---------- docs/reference/ImageDraw.rst | 3 ++- src/PIL/ImageDraw.py | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 Tests/images/imagedraw_triangle_width.png diff --git a/Tests/images/imagedraw_triangle_width.png b/Tests/images/imagedraw_triangle_width.png new file mode 100644 index 0000000000000000000000000000000000000000..3d35326e73b92ffb24b9d64cc771662eee87000c GIT binary patch literal 499 zcmeAS@N?(olHy`uVBq!ia0vp^DImD;uumf=k2YHeTNK0SOcs7 z|DV6msp$lZm}+J5+nH&zb(QAFEMItk*2G_{>lmti1tOXb2(e64*uc`L%Aw$qU_*5&+_>uupeX5C~oixu{%*F`cV5M`^+^t6UwtW;ym>{J}i7cmHX4mfDJwq z^7*X=RE1MQ*4H&8pJJBW`?dD-_sjpAPcv~^--$oPB)K)#^@mrH!?$&-I!`hF^gpP5 zlEE@dmQkZ-LOj3W7puq3J-@zfU|z3hc0x)1)AJh&?jqZS^iDLr&gkVSN^#UqY|`1) zcdbJqeeX_1?T3v$R>iUgB4Q_QhKC;5dYoye(vPc086$r<|C3>U^2%Ifj}Y656|C1U zN--{ZwW?{|61kQzLoZ#AukA7wy5BZkXDqONb1i$vo=pi?8!lYG7o7YVvOvzStWc37?}*7u6{1-oD!M<@Fc=% literal 0 HcmV?d00001 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 7ffd7969d90..406f44c06e3 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1347,20 +1347,20 @@ def test_same_color_outline(): @pytest.mark.parametrize( - "n_sides, rotation, polygon_name", - [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], + "n_sides, polygon_name, args", + [ + (4, "square", {}), + (8, "regular_octagon", {}), + (4, "square_rotate_45", {"rotation": 45}), + (3, "triangle_width", {"width": 5, "outline": "yellow"}), + ], ) -def test_draw_regular_polygon(n_sides, rotation, polygon_name): +def test_draw_regular_polygon(n_sides, polygon_name, args): im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) - filename_base = f"Tests/images/imagedraw_{polygon_name}" - filename = ( - f"{filename_base}.png" - if rotation == 0 - else f"{filename_base}_rotate_{rotation}.png" - ) + filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) bounding_circle = ((W // 2, H // 2), 25) - draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") + draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) assert_image_equal_tofile(im, filename) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index aec7a3ef89f..29115120c13 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -296,7 +296,7 @@ Methods :param width: The line width, in pixels. -.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1) Draws a regular polygon inscribed in ``bounding_circle``, with ``n_sides``, and rotation of ``rotation`` degrees. @@ -311,6 +311,7 @@ Methods (e.g. ``rotation=90``, applies a 90 degree rotation). :param fill: Color to use for the fill. :param outline: Color to use for the outline. + :param width: The line width, in pixels. .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e9ccf80413f..1e4eeab2543 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -279,11 +279,11 @@ def polygon(self, xy, fill=None, outline=None, width=1): self.im.paste(im.im, (0, 0) + im.size, mask.im) def regular_polygon( - self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 ): """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline) + self.polygon(xy, fill, outline, width) def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" From a4986ba9866797648b602ce62fee393f43b522df Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 07:54:30 +1000 Subject: [PATCH 570/727] Support reading signed 8-bit TIFF images --- Tests/images/8bit.s.tif | Bin 0 -> 16518 bytes Tests/test_file_tiff.py | 6 ++++++ src/PIL/TiffImagePlugin.py | 2 ++ 3 files changed, 8 insertions(+) create mode 100644 Tests/images/8bit.s.tif diff --git a/Tests/images/8bit.s.tif b/Tests/images/8bit.s.tif new file mode 100644 index 0000000000000000000000000000000000000000..043cba6af8bbfe6d7386ac7ca247c5235c0dc387 GIT binary patch literal 16518 zcmYj&2VfKD`F_$7u(j?^cc;^qEE^k)S!NRmnFPWNdk^nD@ZQ^&W!bXjz4tcQ?7b5R zfwW1Qj-)NkB2BY51n~d=d?(Xizr?4LZ1lYE^UiPFnl(>Md*X>FCO_f+}ThXe%$1_lHK95@gV5a=4X z2?`Dl3h6D!%heyXJBqb#!9E^`Ys1azLUQ4kiv%u*MTC>R$_$oo~X;A4c z$pOy{K5sCp&3^pLhZZw>i`skM8()6=^@lct*R(16x!%*}&UkiHNKpKNNSCs~5y3$& z^svxSKoMYqA4G0NLhxxY2uMs!N&*83iGqsJ>op42>@k7wWfD|^TIa3f4f+++HXMCg ztugAn_=RK3R4Sv%!*t=7%AAo!8XtpZeRkTad9!BDoVI>{#GV5%L1=X7ZfG7F78V*D z3Vc9;1tLUnFtNhKEeMEBh(Ab1LL#qH0-xeMENUM5n~Yv&g0B@!dO_vIZ|R$*7d*X; z%58%lTD{t0p7GYrnOeqV@~OA2w0;wBP9In|YufyMF+ri>VZp&+k-!ZL4+HkZAR=IZ zf7b|rEWx3XF;UU67;*8OiZueCciCTKGJn{4@1*tF4z~je_v!w@Bv5>79JiR3jYJoA)%q<0wRK3 z)*vJR9~6a0L`FqL$Ha01Z_=w3oYAaS8I2mF(aXds;Q*c{PSB_|oB9`MG*hPNm752A zSZ<2RXXfdi8l8}N?@sIIKaSu3;o&duymj}Azi-Kvro<2+M??TUJUlE!gdxTtcEAV= zf$NKo;PQ{qaNt7#Uad0g)pAy66nKJf@-`EEwV=~z7`<-I;M1JROJmSSw;BYa$q@P1 z2jeB`=hC*M?pqby`{AAL;O+V8D}ycmUq1TfrXwmOA~GTz{KEl=nivp;=nM4`n1uYG zz@QK~XBhCKqhn$O?Li+kPt$r&6*JAE(rbORYNd+d^*TMLR2$Y0&(oSM7K7$Mr_rMJ zyZZB^U&q6ZdPB*DA3m@6{Q946RR8$5KW)^^oO1gA-GB7(?d*t{$e@G!V(- z@Nj@25nl|1pb+rl3JdakI6(w$`STq=HRY)`s5QFjf>x_Hs}zEfHX{w{)y7qW3$m!r)!|)Q1Fte?O;LMTJTx+fQR(ys6(^Wp z9$Kn3d%;7KYnJ@<{ez!A8X5WH5}8Wn9rd?2fBE&-2R}c!|HtlWf?kvGB?uUQ_~6ms z6T-t|gTS|I1P2BN1K9-%zmFichkJyPfgsQtu0Htc?$uRBEwA-Dxz{XMG=gB5tm4!f zJ+IL$AAU|_@I+#cZu!g4kG^ZO#y@RV^Cr!d(|>RL^Zkd99{%!8)8E21oJV0Uq8H#ux24Nv0GCF$r?spG=xSK$GSQvKdpWfTD z;ldv`@M=~$+vd%Ih-U^?2wG3ALA(F2-+viCU%|*_dtyx*#wY8Wf4|fD@vrwU{ppLZ zwoUUfS;9Pgv>O^P-TUVAxAz<*EW3utJsiM=kJO9gNIZ`S5D^sN_th7lfA!bJ9vXui_tM=;?V{w_i-*<;f~Q$+*nHZqR%vvHZ(Ta&ymIG@KmR2) zz{@YQd+CaiW9OgM2>VJG zdZ-zVL{RKTIgSVe(`3Nu#Igk5N!cUV5F-ZlH_qVkbhXK(SF(bJ^}hB^LjLQw7Hej0 zslEA+503Z=nkmhDlv<-wE@Na0l~P9ko~PtmN+ZZvM#FmwM!{$h^i$O8DF^cY@$PyL zo|4fDcS`;gvkGYsSq4;NM2bTc1=!I9n*79J$c@vVMy+QIdZR^q%uhWRZ=Jtd`BEzPkk!wD>}mrznSW-{geq+BMI zQNNqaQp+NN9T_F|Ie>-~fW-}(9^3$3?0ymwxTzY0&r~D6P2gwrp!@YS={QcoWZl`| zU74*DDv~seRK`gaZc3?=QYd8d$+YsfjJr%CnJkgJ%M?;K>7+@tRK`5zE}bN}DSl76 zGqgm$JO=m>AUY;4jsX78h>nbe0QevleYnb`w|Gs}ayqrr=(pHQ=V|dUswf5H>{BcB z{yHu{MWv8a6eXwRQiV(+n=F${rE=*c2;eT4xs%T%QmMq_~?Y_1Y+cPGSPq> zapcvdID~SEB$y4Jp57{o*J|`?LG5KSdzjP=#Vi)QWbT5BOD?1-nL;k-C^zsw=_#q3 zgr=p?6hj7MEATrR^jAO}+{)!{Qd%yN$!5pKCB`PkB*vr1P9U8&2Id=aqBIZ&a)^`= z{XM7fa*aj_0rXxV0QxH!+N@LZf{r#E7xz@zW(_+ zCWeSfoPd^@pm~3iv0a^6mD(` zN+OX<+1P{0Kut+b#{Z%W0LP>c0wiGanMj0yh)IG;XEuAOXr+!9%)F<`;^ER?P@?_e zCEAR1C0rUzfPCeY?T*&&!J(ny(WA!(M-H_$G}pJaw)c#TjU5{3>FDfl&)x4uPXYr9 znvOpxLZ@Qjh6n)~{%?>8a6&>-ipH!n5qw6=avo}LqseSGkoeQ+)H($$Tacmxr4os| zpfRn=2Y7ea;qzBtd;RK}t2Zy6yL|rQ`SVw<-?(;R^w^Q1p^@&|oD~ABuTXNyKuAhX zNlQyhO-V^fO-+FS7`RV0WM1&Fcxj%S#tLeML9K&B8GuhIl!65l z0ES^x>@ulx((k->?Am;=dFaUK@e^k*Tzliz?d#WXy!FPF*RS4s^Ty5BuAIMc>E@Av zk)gik7JKR5B^2XsOiqkXNk~acPfJZD9Q;=fNHa0g($dq@|5In2Z5{KFpbqFY9(n^- z9bEV(y;k4^WqOfRDN}f?*tj;Rw7Kud*!fFWuU$HO^2Ft9*Kfam;p*kn7tb9T>!>QN zs%&kmYia9ja#rT;G)dTL>1in$$?4e{>1pX185!w#b;*)W-ezRr9r-x@^N)gVezsqy zSdlCkbb7sB&>9UY@UO)rOU-GNS;cbdH}uryYqw=oR5bVXHP=^_78X}Fow)Sc^(&We zUORO9M0;~_UU6zkd*{i{#^&ZaYjMg7UN$Qu9kvF;fX>X!%*ezm0mMCd3*-#)dFH>q z{pRDdQ

_vd+OX$L`f=Gnw@MkZHHd1+60wzayuzrVl5VYQYwboHIS zeCgcPTbD14bvHLB?LDw3BByq==SX!`YePcHn(6Arxw*-y*;#3T%E-(TAv3bFvNE%? zaRIvcd3H|jH=lm=&wWNu$-a-8y-Y!kv$R-*;3x&9RC637XZe(DhNTQE!@}Y+9c?WQ z&CR7rC>#e9>xPeyjhw!5`P`8%XF<@`-5Zv@xO~yG&#YRrXK!Hqrm4nNh52b21-S$` zli)&xtZabil$2a)(NS^*%M~WF z%p{NC*!^Ki)h(4ZHI+G0fq@Yzg=y6TV~0+>ar5d}O;JkFa<3&DgLm&*?>jwix8I6w zd!|p@S(KfcmYJFXFu-SJ65Q+@f{y_a@DAGN<$d-Nnp|U7HKRYcSkig2y%+Jduz*#xDu92PNx;y&iTkn4{pj-0# z-H-nDm%USXtxjvAR{PJc=rXD#48^9WnpjJ4QRJ5A40D(I8KT-*{mHaUfdP6w=kz^NdaB-SJ5cFCTzcM=iE5J}(cS+0J@;a~^tK#`~Xt_P2OXoz6q6VP{9hMOzq!oaUJ9BE!6d zg2M=zMy{B4sPJY zwmTxLzO*_gHY#<+vdYrp%v@V$(Znbwqo@#wg&5=o@)YFft$k)HCwM3+-3_O2=+OiL zE1*2RJRvi9vK-#0RK}Hfyjr$xb9hutcuvH|^{ZZbcI9f!>~l+N>zV_{-ulDYjIsWv zlC0{I+~nA#VtZkEO>bdJytQ&!T5euxeqoV_i%SW{1imN{`3(`kM=h8=%yQY(H`nVt z%?orowZWiXx+AIfRRzs*44smz-W|0eGCn@s*>Es=^~=jvgj5Z6RMb};x-@WL==$3? z`v*@nlpXFrartDot!JRAwWT#bskY6&DXk#KR*+{WxMWzZR#5fG=CP8q7u)S4XU2M)N?Qlpdpo=?~1eMvSQc+ThGir2b1C-H)I{%R6FzT-SYFm*tIk>o>*_oeQ`r7%C z)YfZny>;zY#n7=^=Nsz_>^X&HBmK4Ybya1_o!xuO3aojyl423ObYfUbh#d&=g}8{K z@PYsWXoaA{+N&DtUuK;K6Dz4g$*Sc_hKViWjA^elS0(JT+G|T|G6FzcS>@PUw=VUk zpSt?a?LWSAq~`LC8}HsYH_+I6_-KD~U9}@_RbfYFPDyEDVQHBSnwQ#uZ^gZ}xCA%E z4<@WoTr8;7I+I!<<2eDda2~<{U%@D(Dw@`yu}vwL%uWgJuG+sZzp$>MJ}ten|J;qY z|NMvR@6>i)y8708@7#R5edOw!H*Va#c=Y)3{+9NJjP~LUG40NZa$7+`N%3R+veHtU z4Wfwn&|Ne>$N;_;4G)&o)CRp;1!1%T{2rc9QCbPBim@$jzEsfMxHr|2RZ>@19F%_Q zy|@4LxA*>ZE&KHAw{O4q&aK<0az?LSyYpxbK+tZz% zR9aEv$nL%Q$G2}@I+m7t_}s1AH{O2t-K!~qwFATb14qxF>2sDASM{E5%d}V8tfe5k z2nGPK%|`SuBmWZtKm)-hz5#rVo-`bs8kJA0(-{QRU0Oy_Y9*tUSqgevj=$bF)YN`v zC?~nFu&S%T+HvN5b5U48=dtrQ-@f^WH($SbA}2O6D5S7&^hl4@R$ktDqAI1a%xbd( z&MM-UL4eXyJK=!P;WCA2j8d}5uH|HM5nr#bAjJ2QZ6@twj^~ zWp-PcsJ|$J3m;dT4g3RNPBFY%qtU1ZPR()zp9nxLZ9hCR=D2jdq`K$yU{7gwfx}VK z*fwzNRLkDAp&PcRWIJkV9kGE6Hu!I;97OJEZ|JTst4{1XR@G%KvX+&VSJ*^+ds$h9 zy^P$tC=ij!rL2rH2&@ky#hQ`UjFo?l+Nh>!j&r9J9JjLe(2+yK)tAp&Yldp84`de? zR<<~*+dKN}g4Q-?E_?d9H9K}}TDy1EwweIHU|VfPTWxc1QLhvj5lx&wZTN0 zy`o%91trA$0PONfdwIE@S9ULv>1GFfBa9hx7&G**Am@imXiqvWL}=*S*VZBOKL11YZ^OpczCG(!l}-x_TwXG zFCMX#wf5B1HRWs#Z0T<6Z|(1>%r0tnHr3c#QfEIO9G_p+*45m8>`eRdVFE_H4*HW9 za&f_v;CEF6Q3Ct(Q?l1-RT8O5uF|M99L=dXiesgeN8RC}(cZD4;l758CwsDsPhLH9 zeYC8my{Exp+qb-`t-EieXW-D`?$P$frmm*0y3KxD!tJ${^)TnU08v-m|C9 zrxv$3M{1hlU)W#W*?;`-iP1CHFJC@!yrXxpx20tQD!0Ig6{lc2HYuD@vUcYwzn&(%q3QQ_*={w(B*HrW34o9QC zrsQ$_kq{vGa0akYHW36aDTw}eZWQY{+HK`L%=dM2idU+5IpsNLMoQh-kz<1=jt%xV zojzF-yYcy@3!e7#Np!XkojjhO6}bNSok8JAvAeeh#IqOazIn>%xuwj2` zZ=bCkUkx8$G+8|puDq-XfZa7S%jN9pjf!>#=d z$*)F~AL^2(W}9aSBp-FYn?PV{y;sRttiLKD-hoqg@iWgRVT<(1jn zg3`*W^2#eJAOjiVEA%I5KqW7t00f`#!D<T8`P72zRC@x^83 zm9`2pAj4xpDv%5avg-;0zyP_ZXcZ%2l**|dYFdqsUkL)#)BUH;T2weVJaVLOptJf& ze@beG9dn7iz=hjVYf8(rTRR-3&MJG|z@fg*`UXc~v7@!K!`WSEvu}*bOSM_6%F8M$ z9F>)higE`L0HYHA2U8HgfD|M>ksM?#)k-;oO7a`|B&ABuQb_zb3gA6!w(qPtTbp~4 zf%|*v$GVdb5r*%+G5Sib<{O?^!K$j4PCi)aMq zY^|`C+bSIsL!<#0M}=q(T!g?<}bM5Numyeygbm`3KSe>Id-da+bnP_#C zRaBN&i2_tsRS|p)hrJvXn3Q1%07n#a53#=z6_3I?Hm8IUC^TN0Y5x8*r%#{Zzi4+` zM^8=jIcHN{T3JR`er`#2;ZpD7s?_|ls=~UKj@HJ3!PDo451qPt`AAn=QEF6nR%&TZ zeks9rRyZqQ`U;2hF%O~cX&YL^S&)3g?@v@@!L2Lch zYHNOyE!7^MR<+bRFIHqGhUMl} zB$t#&t1Ne&ZH|5Orp|vcAUQKBe8--EsL0Tu!2R3SF7%$4THTQ;;XNZNN-Ho=a1j6h zzfn~QU{L@UKFNt>usqsrxq_z9_%NJh;oQ0N=L3KGbpJX2bF%6k4Hs%N5`**eN)yU5 zIQPWYk{cX(+bljamacqmk>Aw0&pbDO+SEBuzqo4I;)Blm&RCgJ&hDzIt}KBQR647j zm5&GBi!R_YfLMw|d=B{7)S_9?!22v%G{=A5{JFDd_)ecS*LPlUOGf+EuDI}k;)6LE z!5W!+*Wv9g73C=_UU=r|MJ6?;Li4O*RMTJHy7iTaDqCB7B7??NxvA3G?5wVKI-Ql} z|HX#@6BZzu5p}^eI3Cj-Df(|(WAd22aQaNb|E%fLeCN!axnNCeOwVh>A^Ue1>`s{j z)#MkBJ=0TLoEf}(_m*|DeGFQqT%q=u@p4eazCB5W&dOffWX@eq{eEtKmEBoa6D7488wxTn)=#Gk_h0ARW%ikDtH1ilLIaQ8B|zfbeEZWM#s9!29sAT zojGS7{Lk0hJZpiU7S#x;NjljLTW{E3q}?xn`OK=O#=QKbP6BJ>iK5 z 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 TR=N?=Kr4|!DMe+Lt#1AQlp!)* literal 0 HcmV?d00001 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 97a02ac969f..30c6303a2a9 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -198,6 +198,12 @@ def test_save_unsupported_mode(self, tmp_path): with pytest.raises(OSError): im.save(outfile) + def test_8bit_s(self): + with Image.open("Tests/images/8bit.s.tif") as im: + im.load() + assert im.mode == "L" + assert im.getpixel((50, 50)) == 184 + def test_little_endian(self): with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910abd..1ca1b6ea9af 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -170,6 +170,8 @@ (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (II, 1, (1,), 1, (8,), ()): ("L", "L"), (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (2,), 1, (8,), ()): ("L", "L"), + (MM, 1, (2,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), From 9154a6b22d2915d4ca689054a239ceeed078ef2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 08:01:48 +1000 Subject: [PATCH 571/727] Added release notes --- docs/releasenotes/10.0.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 3ee1a9973cf..1004ba57d79 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -159,7 +159,8 @@ TODO Other Changes ============= -TODO -^^^^ +Support reading signed 8-bit TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +TIFF images with signed integer data, 8 bits per sample and a photometric +interpretaton of BlackIsZero can now be read. From 2467db492e7e50efaf39d445c92190c713e40f6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 08:15:48 +1000 Subject: [PATCH 572/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7b13900a8df..93517e1cd32 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added width argument to ImageDraw regular_polygon #7132 + [radarhere] + +- Support I mode for ImageFilter.BuiltinFilter #7108 + [radarhere] + +- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112 + [radarhere] + +- Added unpacker from I;16B to I;16 #7125 + [radarhere] + - Support float font sizes #7107 [radarhere] From 9f0c4164694ce99e14e7ec7fbfa8938acfb74823 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 15:30:17 +1000 Subject: [PATCH 573/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 93517e1cd32..f8844dacad8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Support reading signed 8-bit TIFF images #7111 + [radarhere] + - Added width argument to ImageDraw regular_polygon #7132 [radarhere] From 3ae321832a52cf185ae2c10a5b17f7ea1c6f2dbe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 15:37:35 +1000 Subject: [PATCH 574/727] Added release notes for #7132 --- docs/releasenotes/10.0.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 1004ba57d79..e2005b710e6 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -135,10 +135,11 @@ TODO API Changes =========== -TODO -^^^^ +Added line width parameter to ImageDraw regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +An optional line ``width`` parameter has been added to +``ImageDraw.Draw.regular_polygon``. API Additions ============= From 5377b0735f44ad78184a1b0092a327e22203a02a Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Thu, 4 May 2023 21:43:57 +0530 Subject: [PATCH 575/727] add _repr_jpg_ for ipython display Signed-off-by: Ishant Mrinal Haloi --- Tests/test_file_jpeg.py | 13 +++++++++++++ src/PIL/Image.py | 23 ++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 73a00386f6f..3676c8f0746 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -922,6 +922,19 @@ def closure(mode, *args): im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False + def test_repr_jpg(self): + im = hopper() + + with Image.open(BytesIO(im._repr_jpg_())) as repr_jpg: + assert repr_jpg.format == "JPEG" + assert_image_equal(im, repr_jpg) + + def test_repr_jpg_error(self): + im = hopper("F") + + with pytest.raises(ValueError): + im._repr_jpg_() + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bee9e23d088..557810f6c1f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -633,19 +633,36 @@ def _repr_pretty_(self, p, cycle): ) ) - def _repr_png_(self): + def _repr_image(self, format): """iPython display hook support + :param format: Image format. :returns: png version of the image as bytes """ b = io.BytesIO() try: - self.save(b, "PNG") + self.save(b, format) except Exception as e: - msg = "Could not save to PNG for display" + msg = f"Could not save to {format} for display" raise ValueError(msg) from e return b.getvalue() + def _repr_png_(self): + """iPython display hook support for PNG format. + + :returns: png version of the image as bytes + """ + return self._repr_image("PNG") + + def _repr_jpg_(self): + """iPython display hook support for JPEG format. + + :returns: jpg version of the image as bytes + """ + return self._repr_image("JPEG") + + _repr_jpeg_ = _repr_jpg_ + @property def __array_interface__(self): # numpy array interface support From c5f90af56c7ca2b0e1ee3d3a95b5e7cd5291df5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 07:19:13 +1000 Subject: [PATCH 576/727] Updated xz to 5.4.3 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9b5fc5d1840..05df77a6886 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ def cmd_msbuild( "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download", - "filename": "xz-5.4.2.tar.gz", - "dir": "xz-5.4.2", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download", + "filename": "xz-5.4.3.tar.gz", + "dir": "xz-5.4.3", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), From 93e507294bb5f54ea8ac43a596fd0d2677e2cdad Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 08:19:43 +1000 Subject: [PATCH 577/727] Only assert image is similar --- Tests/test_file_jpeg.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3676c8f0746..0247527f5b2 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -922,18 +922,18 @@ def closure(mode, *args): im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_repr_jpg(self): + def test_repr_jpeg(self): im = hopper() - with Image.open(BytesIO(im._repr_jpg_())) as repr_jpg: - assert repr_jpg.format == "JPEG" - assert_image_equal(im, repr_jpg) + with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: + assert repr_jpeg.format == "JPEG" + assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpg_error(self): + def test_repr_jpeg_error(self): im = hopper("F") with pytest.raises(ValueError): - im._repr_jpg_() + im._repr_jpeg_() @pytest.mark.skipif(not is_win32(), reason="Windows only") From 04191d15f6fee33c50536991e734454195c2da8a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 17:54:42 +1000 Subject: [PATCH 578/727] Removed separate test for array tobytes() --- Tests/test_imagepath.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 7a517b6f61d..5082f9a79c0 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -63,17 +63,6 @@ def test_path_constructors(coords): assert list(p) == [(0.0, 1.0)] -def test_path_constructor_text(): - # Arrange - arr = array.array("f", (0, 1)) - - # Act - p = ImagePath.Path(arr.tobytes()) - - # Assert - assert list(p) == [(0.0, 1.0)] - - @pytest.mark.parametrize( "coords", ( From 17fbafb10b6fbb7d364ff4e6474149c12bc03a42 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 18:12:10 +1000 Subject: [PATCH 579/727] Updated ImagePath tolist() default --- docs/reference/ImagePath.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 7c1a3ad7017..500096ef7dc 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -48,7 +48,7 @@ vector data. Path objects can be passed to the methods on the Maps the path through a function. -.. py:method:: PIL.ImagePath.Path.tolist(flat=0) +.. py:method:: PIL.ImagePath.Path.tolist(flat=False) Converts the path to a Python list [(x, y), …]. From 38c40d81d2d0a97e208c7f3bcf468a81176f6288 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 18:25:05 +1000 Subject: [PATCH 580/727] Use boolean instead of integer --- Tests/test_imagepath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8f8a9f44915..5c40d475619 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -28,7 +28,7 @@ def test_path(): (6.0, 7.0), (8.0, 9.0), ] - assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) From 2d841e16c2d7b16f6fe0b156c79a13affd9ac630 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 6 May 2023 10:31:58 +0530 Subject: [PATCH 581/727] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 557810f6c1f..3c0094817c5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -637,7 +637,7 @@ def _repr_image(self, format): """iPython display hook support :param format: Image format. - :returns: png version of the image as bytes + :returns: image as bytes, saved into the given format. """ b = io.BytesIO() try: From ccdce1791dab1e754df17d21a771c8ac073b7c58 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 6 May 2023 10:35:28 +0530 Subject: [PATCH 582/727] rename format to image_format --- src/PIL/Image.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3c0094817c5..33984e59416 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -633,17 +633,17 @@ def _repr_pretty_(self, p, cycle): ) ) - def _repr_image(self, format): + def _repr_image(self, image_format): """iPython display hook support - :param format: Image format. + :param image_format: Image format. :returns: image as bytes, saved into the given format. """ b = io.BytesIO() try: - self.save(b, format) + self.save(b, image_format) except Exception as e: - msg = f"Could not save to {format} for display" + msg = f"Could not save to {image_format} for display" raise ValueError(msg) from e return b.getvalue() From f67fcf131a53f7436a2f4a540ed251c927af2c05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 May 2023 11:58:05 +1000 Subject: [PATCH 583/727] If the clipboard fails to open on Windows, wait and try again --- src/display.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/display.c b/src/display.c index e8e7b62c2e5..754a6ae78d3 100644 --- a/src/display.c +++ b/src/display.c @@ -437,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; if (!OpenClipboard(NULL)) { - PyErr_SetString(PyExc_OSError, "failed to open clipboard"); - return NULL; + // Maybe the clipboard is temporarily in use by another process. + // Wait and try again + Sleep(500); + + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } } // find best format as set by clipboard owner From 2f896ee4ac1f86cd05659c6a3053d38ed0b15aff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 May 2023 21:16:34 +1000 Subject: [PATCH 584/727] Clarify that line() and polygon() include xy pixels --- docs/reference/ImageDraw.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 29115120c13..524f821fb38 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -243,6 +243,7 @@ Methods .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the ``xy`` list. + The coordinate pixels are included in the drawn line. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -287,7 +288,7 @@ Methods The polygon outline consists of straight lines between the given coordinates, plus a straight line between the last and the first - coordinate. + coordinate. The coordinate pixels are included in the drawn polygon. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. From bb18abc603a285b9c1c3705f03d7ee3955f935d7 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 8 May 2023 22:30:11 +0100 Subject: [PATCH 585/727] prefer screenshots using XCB over gnome-screenshot --- docs/reference/ImageGrab.rst | 5 +++-- src/PIL/ImageGrab.py | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 3086ba8c311..6437307c0ce 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -15,8 +15,9 @@ or the clipboard to a PIL image memory. returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied. - On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it - is installed. To capture the default X11 display instead, pass ``xdisplay=""``. + On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return + a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is + installed. To disable this behaviour, pass ``xdisplay=""`` instead. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 2592ba2df3d..db993836d3a 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -61,7 +61,17 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im - elif shutil.which("gnome-screenshot"): + try: + if not Image.core.HAVE_XCB: + msg = "Pillow was built without XCB support" + raise OSError(msg) + size, data = Image.core.grabscreen_x11(xdisplay) + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im + except OSError: + if xdisplay is None and shutil.which("gnome-screenshot"): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) @@ -73,15 +83,8 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N im.close() return im_cropped return im - # use xdisplay=None for default display on non-win32/macOS systems - if not Image.core.HAVE_XCB: - msg = "Pillow was built without XCB support" - raise OSError(msg) - size, data = Image.core.grabscreen_x11(xdisplay) - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im + else: + raise def grabclipboard(): From a0b691a219d274a561784781476cc952e79c1b8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 May 2023 12:12:16 +1000 Subject: [PATCH 586/727] Fixed combining single duration across duplicate PNG frames --- Tests/test_file_apng.py | 6 ++++++ src/PIL/PngImagePlugin.py | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f78c086eb0c..c62231cd4ea 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,6 +440,12 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 + # test removal of duplicated frames with a single duration + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 82a74b26785..aaf242b1d54 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") ): - if isinstance(duration, (list, tuple)): - previous["encoderinfo"]["duration"] += encoderinfo["duration"] + previous["encoderinfo"]["duration"] += encoderinfo.get( + "duration", duration + ) continue else: bbox = None + if "duration" not in encoderinfo: + encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) # animation control @@ -1175,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo.get("duration", duration))) + frame_duration = int(round(encoderinfo["duration"])) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control From b9b685fc5699cbeb18f18096e53a1130d1cd6789 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 May 2023 12:35:59 +1000 Subject: [PATCH 587/727] Updated harfbuzz to 7.3.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9b5fc5d1840..1e5a54f647e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -337,9 +337,9 @@ def cmd_msbuild( "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.2.0.zip", - "filename": "harfbuzz-7.2.0.zip", - "dir": "harfbuzz-7.2.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip", + "filename": "harfbuzz-7.3.0.zip", + "dir": "harfbuzz-7.3.0", "license": "COPYING", "build": [ *cmds_cmake( From c68c508e27935f31ffa357e7e3bc7763cbe461e5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 May 2023 13:25:35 +1000 Subject: [PATCH 588/727] Fixed joined corners for odd dimensions --- .../images/imagedraw_rounded_rectangle_x_odd.png | Bin 0 -> 565 bytes .../images/imagedraw_rounded_rectangle_y_odd.png | Bin 0 -> 527 bytes Tests/test_imagedraw.py | 2 ++ src/PIL/ImageDraw.py | 4 ++-- 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_x_odd.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_y_odd.png diff --git a/Tests/images/imagedraw_rounded_rectangle_x_odd.png b/Tests/images/imagedraw_rounded_rectangle_x_odd.png new file mode 100644 index 0000000000000000000000000000000000000000..f23f1945e1fec32362830cc6e872c08956fe7758 GIT binary patch literal 565 zcmeAS@N?(olHy`uVBq!ia0vp^DImiE{w+sGn_n7?5SDmN3P}2njzRbCo#{c2^^YHh}*S}w5dCT|M zE9R}USk2jA&%3xWWowR(cyO##q+A$Fa4b`#&^6JO{qORB7e5qTwD-`)Up}=9!fJO| zY>HUZcg1$uF22YK;R<2iS#wsda}|3i+FGjhGgMXbv*XuKnWT5Ms_^&yc>Az@ zA4^o_6StUTA+6Q{U6F?LqSp{H{qj(<0ZTvZx)z3Ao!v58r+w;qX>O8ZUel0MKZ z`qzKc(vOPHX4$`@a&I0tNU%*>yN%8LYyLH*?5$5_)&3M;tIrb2-hRV-?bT~}OD0-R zJS)WH&7reUam`oxi0F04#b&FrOcRK3O5kiHi@}@tc3)ZP)6>g>Z}FD>zMYf)GjX6^Gd-AAG-p+a`)s@1J==Xy$i$t|mNo$vAZ{+h@*&h+msciMi&SuU3; zteYmFzL~S|*?fWJo?L}~D+Saaa20ObB(S`UyHL(X5C|?#{>56pvSWg^N}?GsIWTy- L`njxgN@xNA*hcbx literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_y_odd.png b/Tests/images/imagedraw_rounded_rectangle_y_odd.png new file mode 100644 index 0000000000000000000000000000000000000000..96441bc7289eb3e70f23df0f9569a72ca407046d GIT binary patch literal 527 zcmeAS@N?(olHy`uVBq!ia0vp^DIm66s@eeHX4ax2z2 zHumxGnrmG<+MA`k>ge%ZwwI%FHl2v*Kea_{s*cXIrB|NMkUq8kp?mumu8z`qnE?xw zpFgyk{PmcXW@+KFt&>x1_wGuPox02L@~uFla{c->cZ2v(ZrL{b%=OoYw>Rl5oz@)s zv#zi@NmVP<;DC1H)>?8~Y_h7tw1x<#B+d>5Bjd=enzOk-pU;aiv$pylHUFfL zfVYE=qxH2T+jU>IZ8eA#@?T@wc6uXs@S4--s_&fG#Vopy=IX&tG`Z(a= x1 - x0 + full_x = d >= x1 - x0 - 1 if full_x: # The two left and two right corners are joined d = x1 - x0 - full_y = d >= y1 - y0 + full_y = d >= y1 - y0 - 1 if full_y: # The two top and two bottom corners are joined d = y1 - y0 From 3ec03c6720e4a4a0d4bd5a893ccac6b1df14418a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 10 May 2023 13:53:55 +1000 Subject: [PATCH 589/727] Only check for gnome-screenshot on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> --- src/PIL/ImageGrab.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index db993836d3a..3610771100d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -71,7 +71,11 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N im = im.crop(bbox) return im except OSError: - if xdisplay is None and shutil.which("gnome-screenshot"): + if ( + xdisplay is None + and sys.platform not in ("darwin", "win32") + and shutil.which("gnome-screenshot") + ): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) From 8bbccba8250d4ec05a88fce3381f85c7dbb882e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 08:13:33 +1000 Subject: [PATCH 590/727] Updated redirected URL --- docs/deprecations.rst | 2 +- docs/releasenotes/10.0.0.rst | 2 +- docs/releasenotes/9.2.0.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 45b2f42000f..62687d869e8 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -210,7 +210,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. Image.coerce_e ~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index e2005b710e6..d71ca0fa660 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -117,7 +117,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. Image.coerce_e ^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 8d8bfc9f896..b875edf8e5c 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -15,7 +15,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed in Pillow 10 (2023-07-01). Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. FreeTypeFont.getmask2 fill parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 848fd7c2dbaf07bc8e0a24d8efe1400cc8140f99 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 16:49:08 +1000 Subject: [PATCH 591/727] Added linkcheck_allowed_redirects --- docs/conf.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 2ebcd6b2e10..a2c825292f7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -317,6 +317,17 @@ def setup(app): app.add_css_file("css/dark.css") +linkcheck_allowed_redirects = { + r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501 + r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501 + r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501 + r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501 + r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", + r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501 + r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501 + r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501 +} + # sphinx.ext.extlinks # This config is a dictionary of external sites, # mapping unique short aliases to a base URL and a prefix. From 7e29efd518b0b5b2d3c5ed25446f12cf90f63338 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 20:08:10 +1000 Subject: [PATCH 592/727] Do not catch OSError raised when loading image --- src/PIL/ImageGrab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 3610771100d..a51294cb533 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -66,10 +66,6 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N msg = "Pillow was built without XCB support" raise OSError(msg) size, data = Image.core.grabscreen_x11(xdisplay) - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im except OSError: if ( xdisplay is None @@ -89,6 +85,11 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N return im else: raise + else: + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im def grabclipboard(): From 46708099b10a99b3d35d7eae624e83daa6d67bf9 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Fri, 12 May 2023 21:56:40 +0530 Subject: [PATCH 593/727] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 33984e59416..21305d52a4e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -654,15 +654,13 @@ def _repr_png_(self): """ return self._repr_image("PNG") - def _repr_jpg_(self): + def _repr_jpeg_(self): """iPython display hook support for JPEG format. :returns: jpg version of the image as bytes """ return self._repr_image("JPEG") - _repr_jpeg_ = _repr_jpg_ - @property def __array_interface__(self): # numpy array interface support From e063ed772c0be2b878e97d55b69d59c573cd40fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 11:02:53 +1000 Subject: [PATCH 594/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f8844dacad8..7dd99af995b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 + [radarhere] + - Support reading signed 8-bit TIFF images #7111 [radarhere] From 2db9c68571f3be1d29888b3497f4e2af518a2d36 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 13 May 2023 07:32:02 +0530 Subject: [PATCH 595/727] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 21305d52a4e..3522ff6d017 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -634,8 +634,7 @@ def _repr_pretty_(self, p, cycle): ) def _repr_image(self, image_format): - """iPython display hook support - + """Helper function for iPython display hook :param image_format: Image format. :returns: image as bytes, saved into the given format. """ @@ -657,7 +656,7 @@ def _repr_png_(self): def _repr_jpeg_(self): """iPython display hook support for JPEG format. - :returns: jpg version of the image as bytes + :returns: jpeg version of the image as bytes """ return self._repr_image("JPEG") From 59b7a48570cd9c7a2fae1e0876c5072bdd780338 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 12:24:50 +1000 Subject: [PATCH 596/727] Updated docstrings --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3522ff6d017..105c83a8b95 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -634,7 +634,8 @@ def _repr_pretty_(self, p, cycle): ) def _repr_image(self, image_format): - """Helper function for iPython display hook + """Helper function for iPython display hook. + :param image_format: Image format. :returns: image as bytes, saved into the given format. """ @@ -1122,7 +1123,6 @@ def quantize( Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` (default). :returns: A new image - """ self.load() From 6df8716025686fab55b7565df489e01f3a23e5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 21:38:01 +1000 Subject: [PATCH 597/727] Update grabclipboard() documentation after #6783 --- docs/reference/ImageGrab.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 3086ba8c311..10c580a7438 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -39,9 +39,11 @@ or the clipboard to a PIL image memory. .. py:function:: grabclipboard() - Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported. + Take a snapshot of the clipboard image, if any. - .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS) + On Linux, ``wl-paste`` or ``xclip`` is required. + + .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) :return: On Windows, an image, a list of filenames, or None if the clipboard does not contain image data or filenames. @@ -49,3 +51,5 @@ or the clipboard to a PIL image memory. On Mac, an image, or None if the clipboard does not contain image data. + + On Linux, an image. From f3283837630e25f88f1d8f73961c898d88ab1aee Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sun, 14 May 2023 11:11:56 +0530 Subject: [PATCH 598/727] Apply suggestions from code review Co-authored-by: Hugo van Kemenade --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 105c83a8b95..e0fb6a8858c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -650,14 +650,14 @@ def _repr_image(self, image_format): def _repr_png_(self): """iPython display hook support for PNG format. - :returns: png version of the image as bytes + :returns: PNG version of the image as bytes """ return self._repr_image("PNG") def _repr_jpeg_(self): """iPython display hook support for JPEG format. - :returns: jpeg version of the image as bytes + :returns: JPEG version of the image as bytes """ return self._repr_image("JPEG") From 9754c8d18dc8f013193d18810b9de4f2b68694ff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 May 2023 22:42:39 +1000 Subject: [PATCH 599/727] Added release notes --- docs/releasenotes/10.0.0.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index e2005b710e6..ececfa20d74 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -160,6 +160,18 @@ TODO Other Changes ============= +Support display_jpeg() in IPython +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now +also be used to display images in IPython:: + + from PIL import Image + from IPython.display import display_jpeg + + im = Image.new("RGB", (100, 100), (255, 0, 0)) + display_jpeg(im) + Support reading signed 8-bit TIFF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From ce22ad96b71dd3a64ed65d00f36640b81beafd31 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 May 2023 09:47:09 +1000 Subject: [PATCH 600/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7dd99af995b..b3a6c45a4d3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Prefer screenshots using XCB over gnome-screenshot #7143 + [nulano, radarhere] + - Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 [radarhere] From 82e57b8a90bf0c062f6fb87b4bc3ed465e2b2c88 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 28 Apr 2023 12:53:17 +0300 Subject: [PATCH 601/727] Build only PDF in addition to default html --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index ec3300dd1f2..bda03d94457 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,6 +1,6 @@ version: 2 -formats: all +formats: [pdf] build: os: ubuntu-22.04 From ac2d283065e38672a844730e0b11dd9835d588b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 May 2023 07:08:02 +1000 Subject: [PATCH 602/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b3a6c45a4d3..c67b9b432e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Use "/sbin/ldconfig" if ldconfig is not found #7068 + [radarhere] + - Prefer screenshots using XCB over gnome-screenshot #7143 [nulano, radarhere] From 53e73fd0941a8148e293245dfe9edf8412dacbaa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 08:21:59 +1000 Subject: [PATCH 603/727] Updated fribidi to 1.0.13 --- winbuild/build_prepare.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1e5a54f647e..19552f3c787 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -352,12 +352,12 @@ def cmd_msbuild( "libs": [r"*.lib"], }, "fribidi": { - "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip", - "filename": "fribidi-1.0.12.zip", - "dir": "fribidi-1.0.12", + "url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip", + "filename": "fribidi-1.0.13.zip", + "dir": "fribidi-1.0.13", "license": "COPYING", "build": [ - cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), + cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), *cmds_cmake("fribidi"), ], From 0e21e47768315f6af1afcbd88ee24a4ab68b5d36 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 08:25:25 +1000 Subject: [PATCH 604/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c67b9b432e9..c79274d6490 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added `_repr_jpeg_` for IPython display_jpeg #7135 + [n3011, radarhere, nulano] + - Use "/sbin/ldconfig" if ldconfig is not found #7068 [radarhere] From 599979caae111c22bd15f0103320bf74eb53d963 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 16:53:42 +1000 Subject: [PATCH 605/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c79274d6490..626b8b231fe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- -- Added `_repr_jpeg_` for IPython display_jpeg #7135 +- Added _repr_jpeg_() for IPython display_jpeg #7135 [n3011, radarhere, nulano] - Use "/sbin/ldconfig" if ldconfig is not found #7068 From b39c807dde8909b1ce4afd85f37563165224e073 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 May 2023 22:14:40 +1000 Subject: [PATCH 606/727] Removed rectangle example from co-ordinate system documentation --- docs/handbook/concepts.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index e40ed4687af..e0975a12132 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -95,9 +95,8 @@ in the upper left corner. Note that the coordinates refer to the implied pixel corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5). Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles -are represented as 4-tuples, with the upper left corner given first. For -example, a rectangle covering all of an 800x600 pixel image is written as (0, -0, 800, 600). +are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given +first. Palette ------- From 509671c53e31d2fc9af6f89d1ac158cce8160d62 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 17:59:33 -0500 Subject: [PATCH 607/727] fix INT64 def and add warning if not set --- src/libImaging/ImPlatform.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca98c..f183c3aa407 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -61,7 +61,9 @@ #if SIZEOF_LONG == 8 #define INT64 long #elif SIZEOF_LONG_LONG == 8 -#define INT64 long +#define INT64 long long +#else +#warning Cannot find required 64-bit integer type #endif #define INT8 signed char From 6de5e999bd7ee571877c975c9cb2a3038ee90f27 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:00:45 -0500 Subject: [PATCH 608/727] add UINT64 def if INT64 is defined --- src/libImaging/ImPlatform.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f183c3aa407..522776b58cf 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -71,6 +71,9 @@ #define UINT16 unsigned INT16 #define UINT32 unsigned INT32 +#ifdef INT64 +#define UINT64 unsigned INT64 +#endif #endif From e9cfe4b6a2139923e3cab44bdd55cbf45ac16333 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:02:41 -0500 Subject: [PATCH 609/727] label preprocessor if..else..endif for clarity --- src/libImaging/ImPlatform.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 522776b58cf..6db251bf155 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -25,7 +25,7 @@ #endif #endif -#if defined(_WIN32) || defined(__CYGWIN__) +#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */ #define WIN32_LEAN_AND_MEAN #include @@ -37,7 +37,7 @@ #undef WIN32 #endif -#else +#else /* WIN */ /* For System that are not Windows, we'll need to define these. */ #if SIZEOF_SHORT == 2 @@ -75,7 +75,7 @@ #define UINT64 unsigned INT64 #endif -#endif +#endif /* WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ #define FLOAT16 UINT16 From c2527348ecf4487be76fa55eefa440be1a96e9f5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:04:10 -0500 Subject: [PATCH 610/727] add comment explaining why #define and not typedef --- src/libImaging/ImPlatform.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 6db251bf155..e2d2d597b2e 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -39,6 +39,9 @@ #else /* WIN */ /* For System that are not Windows, we'll need to define these. */ +/* We have to define them instead of using typedef because the JPEG lib also + defines their own types with the same names, so we need to be able to undef + ours before including the JPEG code. */ #if SIZEOF_SHORT == 2 #define INT16 short From fbec8f19dd1ec77e3cf741c11038f0229967830c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:11:02 -0500 Subject: [PATCH 611/727] add check for C99+ to use their defs if possible --- src/libImaging/ImPlatform.h | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index e2d2d597b2e..9f736ed75a7 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -43,6 +43,23 @@ defines their own types with the same names, so we need to be able to undef ours before including the JPEG code. */ +#if __STDC_VERSION__ >= 199901L /* C99+ */ + +#include + +#define INT8 int8_t +#define UINT8 uint8_t +#define INT16 int16_t +#define UINT16 uint16_t +#define INT32 int32_t +#define UINT32 uint32_t +#ifdef INT64_MAX +#define INT64 int64_t +#define UINT64 uint64_t +#endif + +#else /* C99+ */ + #if SIZEOF_SHORT == 2 #define INT16 short #elif SIZEOF_INT == 2 @@ -78,6 +95,8 @@ #define UINT64 unsigned INT64 #endif +#endif /* C99+ */ + #endif /* WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ From 9da0b58eea9d59d725106a76702b592e302c9665 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:13:50 -0500 Subject: [PATCH 612/727] move INT8 def to top --- src/libImaging/ImPlatform.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 9f736ed75a7..303ebf9871e 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -60,6 +60,8 @@ #else /* C99+ */ +#define INT8 signed char + #if SIZEOF_SHORT == 2 #define INT16 short #elif SIZEOF_INT == 2 @@ -86,9 +88,7 @@ #warning Cannot find required 64-bit integer type #endif -#define INT8 signed char #define UINT8 unsigned char - #define UINT16 unsigned INT16 #define UINT32 unsigned INT32 #ifdef INT64 From 724f2664601708dd1355c8a03b3815d56d788609 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:14:55 -0500 Subject: [PATCH 613/727] change INT16 def failure to an error --- src/libImaging/ImPlatform.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 303ebf9871e..912b1785548 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -67,7 +67,7 @@ #elif SIZEOF_INT == 2 #define INT16 int #else -#define INT16 short /* most things works just fine anyway... */ +#error Cannot find required 16-bit integer type #endif #if SIZEOF_SHORT == 4 From f6b516bb068f28134c781b42f4188931f9e11b6b Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 19 May 2023 08:01:02 -0500 Subject: [PATCH 614/727] Adjust C preprocessor block labels Co-authored-by: Hugo van Kemenade --- src/libImaging/ImPlatform.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 912b1785548..10d6905b9d9 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -37,7 +37,7 @@ #undef WIN32 #endif -#else /* WIN */ +#else /* not WIN */ /* For System that are not Windows, we'll need to define these. */ /* We have to define them instead of using typedef because the JPEG lib also defines their own types with the same names, so we need to be able to undef @@ -58,7 +58,7 @@ #define UINT64 uint64_t #endif -#else /* C99+ */ +#else /* < C99 */ #define INT8 signed char @@ -95,9 +95,9 @@ #define UINT64 unsigned INT64 #endif -#endif /* C99+ */ +#endif /* < C99 */ -#endif /* WIN */ +#endif /* not WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ #define FLOAT16 UINT16 From 4f734d295f7ce2e31c650e1971b45dae7f19c8d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 15:38:36 +1000 Subject: [PATCH 615/727] Use --config-settings instead of deprecated --global-option --- .github/workflows/test-mingw.yml | 2 +- MANIFEST.in | 1 + Makefile | 4 ++-- _custom_build/backend.py | 31 +++++++++++++++++++++++++++++++ docs/installation.rst | 24 ++++++++++++------------ pyproject.toml | 4 ++++ 6 files changed, 51 insertions(+), 15 deletions(-) create mode 100755 _custom_build/backend.py create mode 100644 pyproject.toml diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a109ec0d8b5..5a737a1ee89 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -80,7 +80,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . - name: Test Pillow run: | diff --git a/MANIFEST.in b/MANIFEST.in index f51551303f6..606e7e074aa 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,6 +15,7 @@ graft src graft depends graft winbuild graft docs +graft _custom_build # build/src control detritus exclude .appveyor.yml diff --git a/Makefile b/Makefile index e41f3641108..776649b2834 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ install: .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" . + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . python3 selftest.py .PHONY: debug @@ -74,7 +74,7 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null .PHONY: release-test release-test: diff --git a/_custom_build/backend.py b/_custom_build/backend.py new file mode 100755 index 00000000000..31a954824b9 --- /dev/null +++ b/_custom_build/backend.py @@ -0,0 +1,31 @@ +import sys + +from setuptools.build_meta import * # noqa: F401, F403 +from setuptools.build_meta import _BuildMetaBackend + + +class _CustomBuildMetaBackend(_BuildMetaBackend): + def run_setup(self, setup_script="setup.py"): + if self.config_settings: + flags = [] + for key in ("enable", "disable", "vendor"): + settings = self.config_settings.get(key) + if settings: + if not isinstance(settings, list): + settings = [settings] + for value in settings: + flags.append("--" + key + "-" + value) + if self.config_settings.get("debug") == "true": + flags.append("--debug") + if flags: + sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:] + return super().run_setup(setup_script) + + def build_wheel( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_wheel(wheel_directory, config_settings, metadata_directory) + + +build_wheel = _CustomBuildMetaBackend().build_wheel diff --git a/docs/installation.rst b/docs/installation.rst index ad27b67eeaf..514d20e7422 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -380,40 +380,40 @@ Build Options using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. -* Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``, - ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, - ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``. +* Config settings: ``-C disable=zlib``, ``-C disable=jpeg``, + ``-C disable=tiff``, ``-C disable=freetype``, ``-C disable=raqm``, + ``-C disable=lcms``, ``-C disable=webp``, ``-C disable=webpmux``, + ``-C disable=jpeg2000``, ``-C disable=imagequant``, ``-C disable=xcb``. Disable building the corresponding feature even if the development libraries are present on the building machine. -* Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``, - ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, - ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``. +* Config settings: ``-C enable=zlib``, ``-C enable=jpeg``, + ``-C enable=tiff``, ``-C enable=freetype``, ``-C enable=raqm``, + ``-C enable=lcms``, ``-C enable=webp``, ``-C enable=webpmux``, + ``-C enable=jpeg2000``, ``-C enable=imagequant``, ``-C enable=xcb``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Build flags: ``--vendor-raqm``, ``--vendor-fribidi``. +* Config settings: ``-C vendor=raqm``, ``-C vendor=fribidi``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``--disable-platform-guessing``. Skips all of the +* Build flag: ``-C disable=platform-guessing``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). -* Build flag: ``--debug``. Adds a debugging flag to the include and +* Build flag: ``-C debug=true``. Adds a debugging flag to the include and library search process to dump all paths searched for and found to stdout. Sample usage:: - python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" + python3 -m pip install --upgrade Pillow -C enable=[feature] Platform Support ---------------- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..cf31b6407ee --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[build-system] +requires = ["setuptools >= 40.8.0", "wheel"] +build-backend = "backend" +backend-path = ["_custom_build"] From 18da2d0b2d01ab7bf9371451416379c0edca84f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 15:19:58 +1000 Subject: [PATCH 616/727] Removed inplace target --- Makefile | 5 ----- tox.ini | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 776649b2834..57d756b47e3 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,6 @@ help: @echo " docserve run an HTTP server on the docs directory" @echo " html make HTML docs" @echo " htmlview open the index page built by the html target in your browser" - @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" @echo " lint run the lint checks" @@ -54,10 +53,6 @@ help: @echo " release-test run code and package tests before release" @echo " test run tests on installed Pillow" -.PHONY: inplace -inplace: clean - python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" . - .PHONY: install install: python3 -m pip -v install . diff --git a/tox.ini b/tox.ini index 458a0010734..a79089f5177 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ extras = tests commands = make clean - {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . + {envpython} -m pip install . {envpython} selftest.py {envpython} -m pytest -W always {posargs} allowlist_externals = From 546f6cbc27e178bfc742d8216a1af508b3e26e02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 17:11:43 +1000 Subject: [PATCH 617/727] Replaced absolute PIL import with relative import --- src/PIL/IcnsImagePlugin.py | 4 ++-- src/PIL/ImageCms.py | 6 +++--- src/PIL/ImageShow.py | 2 +- src/PIL/SpiderImagePlugin.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index c2f050eddb3..27cb89f735e 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -22,11 +22,11 @@ import struct import sys -from PIL import Image, ImageFile, PngImagePlugin, features +from . import Image, ImageFile, PngImagePlugin, features enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: - from PIL import Jpeg2KImagePlugin + from . import Jpeg2KImagePlugin MAGIC = b"icns" HEADERSIZE = 8 diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 38cbab19ce7..3a337f9f209 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -18,10 +18,10 @@ import sys from enum import IntEnum -from PIL import Image +from . import Image try: - from PIL import _imagingcms + from . import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -271,7 +271,7 @@ def get_display_profile(handle=None): if sys.platform != "win32": return None - from PIL import ImageWin + from . import ImageWin if isinstance(handle, ImageWin.HDC): profile = core.get_display_profile_win32(handle, 1) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 3f68a2696bf..8b1c3f8bb63 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -17,7 +17,7 @@ import sys from shlex import quote -from PIL import Image +from . import Image _viewers = [] diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index eac27e679bd..5614957c176 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -36,7 +36,7 @@ import struct import sys -from PIL import Image, ImageFile +from . import Image, ImageFile def isInt(f): @@ -191,7 +191,7 @@ def convert2byte(self, depth=255): # returns a ImageTk.PhotoImage object, after rescaling to 0..255 def tkPhotoImage(self): - from PIL import ImageTk + from . import ImageTk return ImageTk.PhotoImage(self.convert2byte(), palette=256) From 053cb3de52dcc5008e5bac96942697a1b7e9e00d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 May 2023 14:38:05 +1000 Subject: [PATCH 618/727] Fixed finding dependencies on Cygwin --- .github/workflows/test-cygwin.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 9a1e46705de..e7ab6466e4e 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -104,7 +104,7 @@ jobs: - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" run: | - SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh + .ci/build.sh - name: Test run: | diff --git a/setup.py b/setup.py index 0b6b0207764..7c1ad6dc5b2 100755 --- a/setup.py +++ b/setup.py @@ -515,6 +515,7 @@ def build_extensions(self): elif sys.platform == "cygwin": # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory + self.compiler.shared_lib_extension = ".dll.a" _add_directory( library_dirs, os.path.join( From dc6d0641b3c5e93b26b214a251754e549b84260a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 May 2023 19:39:25 +1000 Subject: [PATCH 619/727] Updated redirected URLs --- codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index f3afccc1caf..b794632faae 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,9 @@ -# Documentation: https://docs.codecov.io/docs/codecov-yaml +# Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" # https://github.com/codecov/support/issues/363 - # https://docs.codecov.io/docs/comparing-commits + # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true comment: false From fffcb558f64f2350789b67ec5eb55681408a93d5 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Tue, 23 May 2023 18:44:25 +0800 Subject: [PATCH 620/727] Use image/png mime type for ImageGrab (wl-paste) if possible, otherwise the first mime type taken --- src/PIL/ImageGrab.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 3771e6a79b8..b7f416321aa 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -147,15 +147,12 @@ def grabclipboard(): clipboard_mimetypes = output.splitlines() def find_mimetype(): - for mime in Image.MIME.values(): - if mime in clipboard_mimetypes: - return mime + if "image/png" in clipboard_mimetypes: + return "image/png" + if clipboard_mimetypes: + return clipboard_mimetypes[0] - Image.preinit() mimetype = find_mimetype() - if not mimetype: - Image.init() - mimetype = find_mimetype() if mimetype: args.extend(["-t", mimetype]) elif shutil.which("xclip"): From bce0f0d5a64c008b9d9ffbea33e98a79ffdae8c3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 21:25:11 +1000 Subject: [PATCH 621/727] Moved function code inline --- src/PIL/ImageGrab.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index b7f416321aa..7f6d50af429 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -142,17 +142,16 @@ def grabclipboard(): return None else: if shutil.which("wl-paste"): - args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() - clipboard_mimetypes = output.splitlines() - - def find_mimetype(): - if "image/png" in clipboard_mimetypes: - return "image/png" - if clipboard_mimetypes: - return clipboard_mimetypes[0] + mimetypes = output.splitlines() + if "image/png" in mimetypes: + mimetype = "image/png" + elif mimetypes: + mimetype = mimetypes[0] + else: + mimetype = None - mimetype = find_mimetype() + args = ["wl-paste"] if mimetype: args.extend(["-t", mimetype]) elif shutil.which("xclip"): From 26d5f4fcb1fa23c920f42c56e187de092da544a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 21:27:55 +1000 Subject: [PATCH 622/727] Use tuple instead of list --- Tests/test_imagegrab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 065c9c1b583..f8059eca443 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -102,7 +102,7 @@ def test_grabclipboard_png(self): @pytest.mark.skipif( ( sys.platform != "linux" - or not all(shutil.which(cmd) for cmd in ["wl-paste", "wl-copy"]) + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) ), reason="Linux with wl-clipboard only", ) @@ -111,5 +111,5 @@ def test_grabclipboard_wl_clipboard(self, ext): image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) - im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, image_path) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) From b8719033ca91ef57f58128d32df675457431bbce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 22:53:16 +1000 Subject: [PATCH 623/727] Removed unused INT64 definition --- src/libImaging/ImPlatform.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca98c..94781f9ecb1 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -58,12 +58,6 @@ #error Cannot find required 32-bit integer type #endif -#if SIZEOF_LONG == 8 -#define INT64 long -#elif SIZEOF_LONG_LONG == 8 -#define INT64 long -#endif - #define INT8 signed char #define UINT8 unsigned char From 922e239cca2a45d239dd02f0a4b85b72a7918917 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 May 2023 08:55:14 +1000 Subject: [PATCH 624/727] Fixed saving multiple 1 mode images to GIF --- Tests/test_file_gif.py | 13 +++++++++++++ src/PIL/GifImagePlugin.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8522f486aff..0e50ee1abf9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -252,6 +252,19 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 +def test_roundtrip_save_all_1(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == 255 + + @pytest.mark.parametrize( "path, mode", ( diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index eadee1560b3..2f92e946751 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -879,7 +879,7 @@ def _get_palette_bytes(im): :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return im.palette.palette + return im.palette.palette if im.palette else b"" def _get_background(im, info_background): From 117618b01f959f016833158b7b128e896c6d38b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 May 2023 22:47:43 +1000 Subject: [PATCH 625/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 626b8b231fe..190751ad291 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Improved wl-paste mimetype handling in ImageGrab #7094 + [rrcgat, radarhere] + - Added _repr_jpeg_() for IPython display_jpeg #7135 [n3011, radarhere, nulano] From e6d7f1f3477b915d4f2fb7d71d609af74e47a444 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 May 2023 19:52:13 +1000 Subject: [PATCH 626/727] Install setuptools on Windows --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a008801114f..076b8083966 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -65,8 +65,8 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install From 7a5ddc1712240b21d89581602acbb851c3897e4a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 May 2023 10:28:38 +1000 Subject: [PATCH 627/727] Do not test PyQt6 on Python 3.12 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index d5cbd82488b..6e87d386dd0 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -42,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 - if [[ $GHA_PYTHON_VERSION == 3.* ]]; then + if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi From 26e0c81ffb5ffa23711e7a4c244498307e13683c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 22:40:37 +0300 Subject: [PATCH 628/727] Revert "Install setuptools on Windows" This reverts commit e6d7f1f3477b915d4f2fb7d71d609af74e47a444. --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 076b8083966..a008801114f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -65,8 +65,8 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install From 454dfcc1b4a60e4a56b50eaca0944049dacfc893 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 11:52:03 +0300 Subject: [PATCH 629/727] Add minimal pyproject.toml --- .pre-commit-config.yaml | 11 +++++++++++ pyproject.toml | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4882a317faa..f4b695883eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,6 +49,7 @@ repos: hooks: - id: check-merge-conflict - id: check-json + - id: check-toml - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -56,6 +57,16 @@ repos: hooks: - id: sphinx-lint + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 0.11.2 + hooks: + - id: pyproject-fmt + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.13 + hooks: + - id: validate-pyproject + - repo: https://github.com/tox-dev/tox-ini-fmt rev: 1.3.0 hooks: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..8ed72aad7a9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61.2", +] From 18960e8416dc95774b7e2616baf780c62304299d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 12:58:13 +0300 Subject: [PATCH 630/727] Use latest setuptools 67.8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8ed72aad7a9..59eb08fa9bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - "setuptools>=61.2", + "setuptools>=67.8", ] From 9ea7721a71b8b17086d5113861b509ec3bc1454d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 15:16:55 +0300 Subject: [PATCH 631/727] Replace direct invocation of setup.py --- winbuild/build_prepare.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 19552f3c787..12d2efbbc7b 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -569,7 +569,10 @@ def build_pillow(): *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT - r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501 + r'"{python_dir}\{python_exe}" -m pip install . ' + r'--global-option="--vendor-raqm" ' + r'--global-option="--vendor-fribidi" ' + r'--global-option="%*"', ] write_script("build_pillow.cmd", lines) From 3a0881dffe589fe745ccc6f68f4b2b74cf72f15e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 16:55:28 +0300 Subject: [PATCH 632/727] Disable extra quotes --- setup.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 0b6b0207764..522c2099111 100755 --- a/setup.py +++ b/setup.py @@ -847,14 +847,7 @@ def build_extensions(self): if struct.unpack("h", b"\0\1")[0] == 1: defs.append(("WORDS_BIGENDIAN", None)) - if ( - sys.platform == "win32" - and sys.version_info < (3, 9) - and not (PLATFORM_PYPY or PLATFORM_MINGW) - ): - defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) - else: - defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) + defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) self._update_extension("PIL._imaging", libs, defs) From 58b8d6c4efef6874ff8be7a601c1dcec01239163 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 17:13:19 +0300 Subject: [PATCH 633/727] Upgrade pip --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 12d2efbbc7b..d902cc1fb37 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -569,6 +569,7 @@ def build_pillow(): *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT + r'"{python_dir}\{python_exe}" -m pip install --upgrade pip', r'"{python_dir}\{python_exe}" -m pip install . ' r'--global-option="--vendor-raqm" ' r'--global-option="--vendor-fribidi" ' From 07eccd9798387a79db84557102d34de2f2f4c28d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 May 2023 19:14:56 +1000 Subject: [PATCH 634/727] Fixed calling putpalette() on L and LA images before load() --- Tests/test_image_putpalette.py | 8 ++++++++ src/libImaging/Unpack.c | 2 ++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 3b29769a7a4..665e08a7e0e 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -32,6 +32,14 @@ def palette(mode): with pytest.raises(ValueError): palette("YCbCr") + with Image.open("Tests/images/hopper_gray.jpg") as im: + assert im.mode == "L" + im.putpalette(list(range(256)) * 3) + + with Image.open("Tests/images/la.tga") as im: + assert im.mode == "LA" + im.putpalette(list(range(256)) * 3) + def test_imagepalette(): im = hopper("P") diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index a0fa22c7d3e..206403ba6e0 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1552,10 +1552,12 @@ static struct { {"P", "P;4L", 4, unpackP4L}, {"P", "P", 8, copy1}, {"P", "P;R", 8, unpackLR}, + {"P", "L", 8, copy1}, /* palette w. alpha */ {"PA", "PA", 16, unpackLA}, {"PA", "PA;L", 16, unpackLAL}, + {"PA", "LA", 16, unpackLA}, /* true colour */ {"RGB", "RGB", 24, ImagingUnpackRGB}, From 7f0c49a6c76b7f6fb2086823b01e85ed8e30c450 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 28 May 2023 21:14:39 +0300 Subject: [PATCH 635/727] Move pip upgrade to .appveyor.yml --- .appveyor.yml | 1 + winbuild/build_prepare.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 36f5bd0ad68..cb364af559b 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -20,6 +20,7 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' +- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-depends.zip -oc:\ diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d902cc1fb37..12d2efbbc7b 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -569,7 +569,6 @@ def build_pillow(): *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT - r'"{python_dir}\{python_exe}" -m pip install --upgrade pip', r'"{python_dir}\{python_exe}" -m pip install . ' r'--global-option="--vendor-raqm" ' r'--global-option="--vendor-fribidi" ' From d5e03cca885465f28a01feca00583fa1073a36c5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 28 May 2023 21:20:42 +0300 Subject: [PATCH 636/727] Wrap arguments before passing Co-authored-by: nulano --- .appveyor.yml | 2 +- .github/workflows/test-windows.yml | 6 +++--- winbuild/build_prepare.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index cb364af559b..575b6caa603 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -72,7 +72,7 @@ before_deploy: - cd c:\pillow - '%PYTHON%\%EXECUTABLE% -m pip install wheel' - cd c:\pillow\winbuild\ - - c:\pillow\winbuild\build\build_pillow.cmd bdist_wheel + - c:\pillow\winbuild\build\build_pillow.cmd --global-option="bdist_wheel" - cd c:\pillow - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a008801114f..fbfec8c13d9 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -166,8 +166,8 @@ jobs: - name: Build Pillow run: | $FLAGS="" - if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" } - & winbuild\build\build_pillow.cmd $FLAGS install + if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS='--global-option="--disable-imagequant"' } + & winbuild\build\build_pillow.cmd $FLAGS --global-option="install" & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -231,7 +231,7 @@ jobs: ) ) for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% - winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel + winbuild\\build\\build_pillow.cmd --global-option="--disable-imagequant" --global-option="bdist_wheel" shell: cmd - name: Upload wheel diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 12d2efbbc7b..21b6c10a59c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -572,7 +572,7 @@ def build_pillow(): r'"{python_dir}\{python_exe}" -m pip install . ' r'--global-option="--vendor-raqm" ' r'--global-option="--vendor-fribidi" ' - r'--global-option="%*"', + r"%*", ] write_script("build_pillow.cmd", lines) From c45019fe0ccbf54c925aba914329371a7f188a48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 May 2023 12:28:03 +1000 Subject: [PATCH 637/727] Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 --- src/_imagingft.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 78e3f7f104f..80f862bb710 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -132,6 +132,27 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { return NULL; } +#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11 + PyConfig config; + PyConfig_InitPythonConfig(&config); + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etf|nsy#n", + kwlist, + config.filesystem_encoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { + PyConfig_Clear(&config); + return NULL; + } + PyConfig_Clear(&config); +#else if (!PyArg_ParseTupleAndKeywords( args, kw, @@ -147,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &layout_engine)) { return NULL; } +#endif self = PyObject_New(FontObject, &Font_Type); if (!self) { From e01a0195dd9f54b8174f322d47d4f618f0cf6c50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jun 2023 22:53:07 +1000 Subject: [PATCH 638/727] Removed duplicate config --- .editorconfig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 449530717f9..d74549fe2ac 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,10 +13,6 @@ indent_style = space trim_trailing_whitespace = true -[*.rst] -# Four-space indentation -indent_size = 4 - [*.yml] # Two-space indentation indent_size = 2 From ea3e4242d8fd8bb5cfc9e528f863ce16e20b529f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jun 2023 08:07:05 +1000 Subject: [PATCH 639/727] Removed files and types override --- .pre-commit-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4882a317faa..0ddc6beb415 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,9 +4,6 @@ repos: hooks: - id: black args: [--target-version=py38] - # Only .py files, until https://github.com/psf/black/issues/402 resolved - files: \.py$ - types: [] - repo: https://github.com/PyCQA/isort rev: 5.12.0 From 3693b84ba0b44f71119cec73c8517ec32d1774b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jun 2023 09:21:47 +1000 Subject: [PATCH 640/727] Lint fixes --- docs/Guardfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Guardfile b/docs/Guardfile index b689b079aea..6cbf07b0637 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -2,7 +2,7 @@ from livereload.compiler import shell from livereload.task import Task -Task.add('*.rst', shell('make html')) -Task.add('*/*.rst', shell('make html')) -Task.add('Makefile', shell('make html')) -Task.add('conf.py', shell('make html')) +Task.add("*.rst", shell("make html")) +Task.add("*/*.rst", shell("make html")) +Task.add("Makefile", shell("make html")) +Task.add("conf.py", shell("make html")) From 2d0b13b812eea5238a8df6dc03b3fa4a6c55559e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 4 Jun 2023 22:37:17 +1000 Subject: [PATCH 641/727] Swapped config key and value --- _custom_build/backend.py | 31 +++++++++++++++++++++++++++---- docs/installation.rst | 22 +++++++++++----------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 31a954824b9..86fe6081733 100755 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -7,14 +7,37 @@ class _CustomBuildMetaBackend(_BuildMetaBackend): def run_setup(self, setup_script="setup.py"): if self.config_settings: - flags = [] - for key in ("enable", "disable", "vendor"): + + def config_has(key, value): settings = self.config_settings.get(key) if settings: if not isinstance(settings, list): settings = [settings] - for value in settings: - flags.append("--" + key + "-" + value) + return value in settings + + flags = [] + for dependency in ( + "zlib", + "jpeg", + "tiff", + "freetype", + "raqm", + "lcms", + "webp", + "webpmux", + "jpeg2000", + "imagequant", + "xcb", + ): + if config_has(dependency, "enable"): + flags.append("--enable-" + dependency) + elif config_has(dependency, "disable"): + flags.append("--disable-" + dependency) + for dependency in ("raqm", "fribidi"): + if config_has(dependency, "vendor"): + flags.append("--vendor-" + dependency) + if self.config_settings.get("platform-guessing") == "disable": + flags.append("--disable-platform-guessing") if self.config_settings.get("debug") == "true": flags.append("--debug") if flags: diff --git a/docs/installation.rst b/docs/installation.rst index 514d20e7422..6720d2dce6b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -380,28 +380,28 @@ Build Options using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. -* Config settings: ``-C disable=zlib``, ``-C disable=jpeg``, - ``-C disable=tiff``, ``-C disable=freetype``, ``-C disable=raqm``, - ``-C disable=lcms``, ``-C disable=webp``, ``-C disable=webpmux``, - ``-C disable=jpeg2000``, ``-C disable=imagequant``, ``-C disable=xcb``. +* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, + ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, + ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. -* Config settings: ``-C enable=zlib``, ``-C enable=jpeg``, - ``-C enable=tiff``, ``-C enable=freetype``, ``-C enable=raqm``, - ``-C enable=lcms``, ``-C enable=webp``, ``-C enable=webpmux``, - ``-C enable=jpeg2000``, ``-C enable=imagequant``, ``-C enable=xcb``. +* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, + ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, + ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Config settings: ``-C vendor=raqm``, ``-C vendor=fribidi``. +* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``-C disable=platform-guessing``. Skips all of the +* Build flag: ``-C platform-guessing=disable``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). @@ -413,7 +413,7 @@ Build Options Sample usage:: - python3 -m pip install --upgrade Pillow -C enable=[feature] + python3 -m pip install --upgrade Pillow -C [feature]=enable Platform Support ---------------- From e45da2ae17e79cd2e02bc894f3c825f9126101a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Jun 2023 11:07:09 +1000 Subject: [PATCH 642/727] Do not close provided file handles with libtiff --- src/PIL/TiffImagePlugin.py | 10 +--------- src/libImaging/TiffDecode.c | 6 +++++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 1ca1b6ea9af..3476786421f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1253,9 +1253,8 @@ def _load_libtiff(self): # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading # into a string in python. - # libtiff closes the file descriptor, so pass in a dup. try: - fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) + fp = hasattr(self.fp, "fileno") and self.fp.fileno() # flush the file descriptor, prevents error on pypy 2.4+ # should also eliminate the need for fp.tell # in _seek @@ -1305,18 +1304,11 @@ def _load_libtiff(self): # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) - if fp: - try: - os.close(fp) - except OSError: - pass - self.tile = [] self.readonly = 0 self.load_end() - # libtiff closed the fp in a, we need to close self.fp, if possible if close_self_fp: self.fp.close() self.fp = None # might be shared diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 428cd93d278..9361de83479 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -720,7 +720,11 @@ ImagingLibTiffDecode( } decode_err: - TIFFClose(tiff); + if (clientstate->fp) { + TIFFCleanup(tiff); + } else { + TIFFClose(tiff); + } TRACE(("Done Decoding, Returning \n")); // Returning -1 here to force ImageFile.load to break, rather than // even think about looping back around. From 0835be95cbbdc1beaac0dfaccd7a358621f619bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Jun 2023 15:07:11 +1000 Subject: [PATCH 643/727] Added comment --- src/libImaging/TiffDecode.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 9361de83479..35122f18245 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -720,9 +720,14 @@ ImagingLibTiffDecode( } decode_err: + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup if (clientstate->fp) { + // Pillow will manage the closing of the file rather than libtiff + // So only call TIFFCleanup TIFFCleanup(tiff); } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file TIFFClose(tiff); } TRACE(("Done Decoding, Returning \n")); From 97bd53392ce136617ead36c11d50def9d32ab3e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Jun 2023 18:36:41 +1000 Subject: [PATCH 644/727] Do not use temporary file when grabbing clipboard on Linux --- src/PIL/ImageGrab.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 7f6d50af429..39ecdf42096 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,6 +15,7 @@ # See the README file for information on usage and redistribution. # +import io import os import shutil import subprocess @@ -128,8 +129,6 @@ def grabclipboard(): files = data[o:].decode("mbcs").split("\0") return files[: files.index("")] if isinstance(data, bytes): - import io - data = io.BytesIO(data) if fmt == "png": from . import PngImagePlugin @@ -159,13 +158,12 @@ def grabclipboard(): else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) - fh, filepath = tempfile.mkstemp() - err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr - os.close(fh) + p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + err = p.stderr if err: msg = f"{args[0]} error: {err.strip().decode()}" raise ChildProcessError(msg) - im = Image.open(filepath) + data = io.BytesIO(p.stdout) + im = Image.open(data) im.load() - os.unlink(filepath) return im From 3b65261c966648e5d4f87cd49bb12cba5345547d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 17:54:55 +1000 Subject: [PATCH 645/727] Remove temporary file when error is raised --- src/PIL/EpsImagePlugin.py | 7 +++++++ src/PIL/JpegImagePlugin.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c749..bdac874c44f 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -134,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): if gs_windows_binary is not None: if not gs_windows_binary: + try: + os.unlink(outfile) + if infile_temp: + os.unlink(infile_temp) + except OSError: + pass + msg = "Unable to locate Ghostscript on paths" raise OSError(msg) command[0] = gs_windows_binary diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 5dd1a61afe1..dfc7e6e9f56 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -457,6 +457,11 @@ def load_djpeg(self): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: + try: + os.unlink(path) + except OSError: + pass + msg = "Invalid Filename" raise ValueError(msg) From 97df237dc81c930d983b4025b7b3a97d043dfd7c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 18:04:39 +1000 Subject: [PATCH 646/727] Moved test into separate function --- Tests/test_file_apng.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index c62231cd4ea..a22ac581d8d 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,12 +440,6 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 - # test removal of duplicated frames with a single duration - frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) - with Image.open(test_file) as im: - assert im.n_frames == 1 - assert im.info.get("duration") == 1500 - # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) @@ -453,6 +447,17 @@ def test_apng_save_duration_loop(tmp_path): assert im.info.get("duration") == 750 +def test_apng_save_duplicate_duration(tmp_path): + test_file = str(tmp_path / "temp.png") + frame = Image.new("RGB", (1, 1)) + + # Test a single duration is correctly combined across duplicate frames + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64) From 7c533276f28518ec2825e1cae3f0df427b5c565b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 19:53:50 +1000 Subject: [PATCH 647/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 190751ad291..c51f8fb947c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,27 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed combining single duration across duplicate APNG frames #7146 + [radarhere] + +- Remove temporary file when error is raised #7148 + [radarhere] + +- Do not use temporary file when grabbing clipboard on Linux #7200 + [radarhere] + +- If the clipboard fails to open on Windows, wait and try again #7141 + [radarhere] + +- Fixed saving multiple 1 mode frames to GIF #7181 + [radarhere] + +- Replaced absolute PIL import with relative import #7173 + [radarhere] + +- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192 + [radarhere] + - Improved wl-paste mimetype handling in ImageGrab #7094 [rrcgat, radarhere] From 15edb6d625f94e0f7e9047ab76ed08762ab2f53a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Jun 2023 22:33:55 +1000 Subject: [PATCH 648/727] Fixed signedness comparison warning --- src/libImaging/Jpeg2KEncode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 0d7e896b7f8..de8586706e2 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -464,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { } if (!context->num_resolutions) { - while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) { params.numresolution -= 1; } } From da6b2ec28506a132fad9674e1badb1624aed7a8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Jun 2023 10:47:20 +1000 Subject: [PATCH 649/727] Document order of kernel weights --- src/PIL/ImageFilter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 63d6dcf5cec..33bc7cc2e30 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -35,7 +35,7 @@ def filter(self, image): class Kernel(BuiltinFilter): """ - Create a convolution kernel. The current version only + Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. In the current version, kernels can only be applied to @@ -43,9 +43,10 @@ class Kernel(BuiltinFilter): :param size: Kernel size, given as (width, height). In the current version, this must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. + :param kernel: A sequence containing kernel weights. The kernel will + be flipped vertically before being applied to the image. :param scale: Scale factor. If given, the result for each pixel is - divided by this value. The default is the sum of the + divided by this value. The default is the sum of the kernel weights. :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. From 748a4d0fcd517e0e6e86ae15f4be9b0bcf65747d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 14:26:28 +1000 Subject: [PATCH 650/727] Removed unused variable --- src/libImaging/Storage.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7cf00ef3558..128595f6547 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -37,8 +37,6 @@ #include "Imaging.h" #include -int ImagingNewCount = 0; - /* -------------------------------------------------------------------- * Standard image object. */ From aeb6e9909e94d1ad6c86ebf04a6db6cd77e016a3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 15:57:05 +1000 Subject: [PATCH 651/727] Removed unused argument --- src/PIL/Image.py | 2 +- src/_imaging.c | 5 ++--- src/libImaging/Filter.c | 2 +- src/libImaging/Imaging.h | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e0fb6a8858c..fa70f674b21 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1254,7 +1254,7 @@ def _expand(self, xmargin, ymargin=None): if ymargin is None: ymargin = xmargin self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) + return self._new(self.im.expand(xmargin, ymargin)) def filter(self, filter): """ diff --git a/src/_imaging.c b/src/_imaging.c index 281f3a4d2e6..5c6380feed3 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1027,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) { static PyObject * _expand_image(ImagingObject *self, PyObject *args) { int x, y; - int mode = 0; - if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) { + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return NULL; } - return PyImagingNew(ImagingExpand(self->image, x, y, mode)); + return PyImagingNew(ImagingExpand(self->image, x, y)); } static PyObject * diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 4b8d2bf05c9..4dcd368ca80 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -49,7 +49,7 @@ clip32(float in) { } Imaging -ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { +ImagingExpand(Imaging imIn, int xmargin, int ymargin) { Imaging imOut; int x, y; ImagingSectionCookie cookie; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index d9ded185238..beec8a8f2af 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging -ImagingExpand(Imaging im, int x, int y, int mode); +ImagingExpand(Imaging im, int x, int y); extern Imaging ImagingFill(Imaging im, const void *ink); extern int From 389ad11693deb5ea39b8edf0eb47263582a3f5f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 17:10:42 +1000 Subject: [PATCH 652/727] Only call text_layout once in getmask2 --- src/PIL/ImageFont.py | 34 +++---- src/_imagingft.c | 215 ++++++++++++++++++++++++++----------------- 2 files changed, 144 insertions(+), 105 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf520..7b4ca5814fa 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,7 +26,6 @@ # import base64 -import math import os import sys import warnings @@ -551,28 +550,23 @@ def getmask2( :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) if start is None: start = (0, 0) - size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) - offset = offset[0] - stroke_width, offset[1] - stroke_width + im, size, offset = self.font.render( + text, + Image.core.fill, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start[0], + start[1], + Image.MAX_IMAGE_PIXELS, + ) Image._decompression_bomb_check(size) - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) - if min(size): - self.font.render( - text, - im.id, - mode, - direction, - features, - language, - stroke_width, - ink, - start[0], - start[1], - ) return im, offset def font_variant( diff --git a/src/_imagingft.c b/src/_imagingft.c index 80f862bb710..95f12eb5a02 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -551,73 +551,25 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } -static PyObject * -font_getsize(FontObject *self, PyObject *args) { +static int +bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) { int position; /* pen position along primary axis, in 26.6 precision */ int advanced; /* pen position along primary axis, in pixels */ int px, py; /* position of current glyph, in pixels */ int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ - int load_flags; /* FreeType load_flags parameter */ int error; - FT_Face face; FT_Glyph glyph; - FT_BBox bbox; /* glyph bounding box */ - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t i, count; /* glyph_info index and length */ - int horizontal_dir; /* is primary axis horizontal? */ - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - const char *anchor = NULL; - PyObject *features = Py_None; - PyObject *string; - - /* calculate size and bearing for a given string */ - - if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { - return NULL; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - if (anchor == NULL) { - anchor = horizontal_dir ? "la" : "lt"; - } - if (strlen(anchor) != 2) { - goto bad_anchor; - } - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - + FT_BBox bbox; /* glyph bounding box */ + size_t i; /* glyph_info index */ /* * text bounds are given by: * - bounding boxes of individual glyphs * - pen line, i.e. 0 to `advanced` along primary axis * this means point (0, 0) is part of the text bounding box */ - face = NULL; position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { - face = self->face; - if (horizontal_dir) { px = PIXEL(position + glyph_info[i].x_offset); py = PIXEL(glyph_info[i].y_offset); @@ -640,12 +592,14 @@ font_getsize(FontObject *self, PyObject *args) { error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + return 1; } error = FT_Get_Glyph(face->glyph, &glyph); if (error) { - return geterror(error); + geterror(error); + return 1; } FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); @@ -669,13 +623,15 @@ font_getsize(FontObject *self, PyObject *args) { FT_Done_Glyph(glyph); } - if (glyph_info) { - PyMem_Free(glyph_info); - glyph_info = NULL; + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; } x_anchor = y_anchor = 0; - if (face) { + if (count) { if (horizontal_dir) { switch (anchor[0]) { case 'l': // left @@ -693,15 +649,15 @@ font_getsize(FontObject *self, PyObject *args) { } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(self->face->size->metrics.ascender); + y_anchor = PIXEL(face->size->metrics.ascender); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (self->face->size->metrics.ascender + - self->face->size->metrics.descender) / + (face->size->metrics.ascender + + face->size->metrics.descender) / 2); break; case 's': // horizontal baseline @@ -711,7 +667,7 @@ font_getsize(FontObject *self, PyObject *args) { y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(self->face->size->metrics.descender); + y_anchor = PIXEL(face->size->metrics.descender); break; default: goto bad_anchor; @@ -751,17 +707,74 @@ font_getsize(FontObject *self, PyObject *args) { } } } - - return Py_BuildValue( - "(ii)(ii)", - (x_max - x_min), - (y_max - y_min), - (-x_anchor + x_min), - -(-y_anchor + y_max)); + *width = x_max - x_min; + *height = y_max - y_min; + *x_offset = -x_anchor + x_min; + *y_offset = -(-y_anchor + y_max); + return 0; bad_anchor: PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); - return NULL; + return 1; +} + +static PyObject * +font_getsize(FontObject *self, PyObject *args) { + int width, height, x_offset, y_offset; + int load_flags; /* FreeType load_flags parameter */ + int error; + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t count; /* glyph_info length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + if (error) { + return NULL; + } + + return Py_BuildValue( + "(ii)(ii)", + width, + height, + x_offset, + y_offset); } static PyObject * @@ -785,6 +798,7 @@ font_render(FontObject *self, PyObject *args) { unsigned int bitmap_y; /* glyph bitmap y index */ unsigned char *source; /* glyph bitmap source buffer */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + PyObject *image; Imaging im; Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ @@ -795,27 +809,34 @@ font_render(FontObject *self, PyObject *args) { const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; + const char *anchor = NULL; PyObject *features = Py_None; PyObject *string; + PyObject *fill; float x_start = 0; float y_start = 0; + int width, height, x_offset, y_offset; + int horizontal_dir; /* is primary axis horizontal? */ + PyObject *max_image_pixels = Py_None; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziLff:render", + "OO|zzOzizLffO:render", &string, - &id, + &fill, &mode, &dir, &features, &lang, &stroke_width, + &anchor, &foreground_ink_long, &x_start, - &y_start)) { + &y_start, + &max_image_pixels)) { return NULL; } @@ -841,8 +862,41 @@ font_render(FontObject *self, PyObject *args) { if (PyErr_Occurred()) { return NULL; } - if (count == 0) { - Py_RETURN_NONE; + + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (error) { + PyMem_Del(glyph_info); + return NULL; + } + + width += stroke_width * 2 + ceil(x_start); + height += stroke_width * 2 + ceil(y_start); + if (max_image_pixels != Py_None) { + if (width * height > PyLong_AsLong(max_image_pixels) * 2) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); + } + } + + image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); + im = (Imaging)id; + + x_offset -= stroke_width; + y_offset -= stroke_width; + if (count == 0 || width == 0 || height == 0) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); } if (stroke_width) { @@ -859,15 +913,6 @@ font_render(FontObject *self, PyObject *args) { 0); } - im = (Imaging)id; - load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - /* * calculate x_min and y_max * must match font_getsize or there may be clipping! @@ -1064,7 +1109,7 @@ font_render(FontObject *self, PyObject *args) { } FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - Py_RETURN_NONE; + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); glyph_error: if (stroker != NULL) { From 4dcca33d3099e29110b24cc507e8f0799e1d1ab7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 19:06:25 +1000 Subject: [PATCH 653/727] Removed unused arguments --- src/_imagingft.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 80f862bb710..8fc1fa7d085 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -254,9 +254,7 @@ text_layout_raqm( const char *dir, PyObject *features, const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { + GlyphInfo **glyph_info) { size_t i = 0, count = 0, start = 0; raqm_t *rq; raqm_glyph_t *glyphs = NULL; @@ -493,7 +491,7 @@ text_layout( #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { count = text_layout_raqm( - string, self, dir, features, lang, glyph_info, mask, color); + string, self, dir, features, lang, glyph_info); } else #endif { From 16d82c2dfd473836e7903165917584bf938b0345 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 19:37:54 +1000 Subject: [PATCH 654/727] Improved coverage --- Tests/test_imagefont.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 7ea485a55ec..4a40d1d1d85 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -463,6 +463,11 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") +@pytest.mark.parametrize("mode", (None, "1", "RGBA")) +def test_getbbox(font, mode): + assert (0, 4, 12, 16) == font.getbbox("A", mode) + + def test_getbbox_empty(font): # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") From 1756df461561234184611dfe8b42f1b7f33de1f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 20:24:34 +1000 Subject: [PATCH 655/727] Removed unused private method --- src/PIL/ImageFont.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf520..abcb885204e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -226,10 +226,6 @@ def __setstate__(self, state): path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def _multiline_split(self, text): - split_character = "\n" if isinstance(text, str) else b"\n" - return text.split(split_character) - def getname(self): """ :return: A tuple of the font family (e.g. Helvetica) and the font style From 5a0fb8ec127b7b23d13d22d7a8ade5505835435f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 11 Jun 2023 00:05:47 +0300 Subject: [PATCH 656/727] Add Debian 12 Bookworm --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 4f01abe44c2..3bcb8cfbca7 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,6 +39,7 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-x86, + debian-12-bookworm-x86, fedora-37-amd64, fedora-38-amd64, gentoo, diff --git a/docs/installation.rst b/docs/installation.rst index ad27b67eeaf..ac54b037df3 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -448,6 +448,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ +| Debian 12 Bookworm | 3.11 | x86 | ++----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 38 | 3.11 | x86-64 | From c24c1ccf8163c90dfbc3110490846d3113d3418a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 08:52:55 +1000 Subject: [PATCH 657/727] Use "not in" Co-authored-by: Aarni Koskela --- src/PIL/GdImageFile.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/features.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7dda4f14301..bafc43a19d4 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -47,7 +47,7 @@ def _open(self): # Header s = self.fp.read(1037) - if not i16(s) in [65534, 65535]: + if i16(s) not in [65534, 65535]: msg = "Not a valid GD 2.x .gd file" raise SyntaxError(msg) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index fa70f674b21..66b0f0e066f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1731,7 +1731,7 @@ def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): if not isinstance(dest, (list, tuple)): msg = "Destination must be a tuple" raise ValueError(msg) - if not len(source) in (2, 4): + if len(source) not in (2, 4): msg = "Source must be a 2 or 4-tuple" raise ValueError(msg) if not len(dest) == 2: diff --git a/src/PIL/features.py b/src/PIL/features.py index 80a16a75e0c..f14e60cf5d4 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -24,7 +24,7 @@ def check_module(feature): :returns: ``True`` if available, ``False`` otherwise. :raises ValueError: If the module is not defined in this version of Pillow. """ - if not (feature in modules): + if feature not in modules: msg = f"Unknown module {feature}" raise ValueError(msg) From 538971532da065ada6528d441ea39693e108ebf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 08:55:21 +1000 Subject: [PATCH 658/727] Corrected error code Co-authored-by: nulano --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 25a4d3517e1..dbea673f975 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -189,7 +189,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { /* Don't free this before FT_Done_Face */ self->font_bytes = PyMem_Malloc(font_bytes_size); if (!self->font_bytes) { - error = 65; // Out of Memory in Freetype. + error = FT_Err_Out_Of_Memory; } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); From f338f35657baad30295b45353d699cace83d3699 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 10:01:36 +1000 Subject: [PATCH 659/727] Changed inPlace to be keyword-only argument --- src/PIL/ImageOps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index facc30ba0ee..752d132a320 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -576,13 +576,13 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, inPlace=False): +def exif_transpose(image, *, inPlace=False): """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. :param image: The image to transpose. - :param inPlace: Boolean. + :param inPlace: Boolean. Keyword-only argument. If ``True``, the original image is modified in-place, and ``None`` is returned. If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned with the transposition applied. If there is no transposition, a copy of the From 187e9a46af160b6510c0262ef1f52e4c35ce579e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 11:21:43 +1000 Subject: [PATCH 660/727] Improved documention of "corners" argument for rounded_rectangle --- docs/reference/ImageDraw.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 524f821fb38..31f63695ef5 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -328,7 +328,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1) +.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None) Draws a rounded rectangle. @@ -341,6 +341,7 @@ Methods :param width: The line width, in pixels. :param corners: A tuple of whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. + Keyword-only argument. .. versionadded:: 8.2.0 From bae918280d8bd74d2ace512a71eabc76e05a1d0f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:25:12 +1000 Subject: [PATCH 661/727] Changed alpha_only to keyword-only argument Co-authored-by: Hugo van Kemenade --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f5d1206717d..74c1bd7f683 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1279,7 +1279,7 @@ def getbands(self): """ return ImageMode.getmode(self.mode).bands - def getbbox(self, alpha_only=True): + def getbbox(self, *, alpha_only=True): """ Calculates the bounding box of the non-zero regions in the image. From d7c7b832f142aa5a66ee5dd9ad1ce4f7e2afa241 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:25:42 +1000 Subject: [PATCH 662/727] Highlight code Co-authored-by: Hugo van Kemenade --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 74c1bd7f683..8eebd28f970 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1284,8 +1284,8 @@ def getbbox(self, *, alpha_only=True): Calculates the bounding box of the non-zero regions in the image. - :param alpha_only: Optional flag, defaulting to true. - If true and the image has an alpha channel, trim transparent pixels. + :param alpha_only: Optional flag, defaulting to ``True``. + If ``True`` and the image has an alpha channel, trim transparent pixels. Otherwise, trim pixels when all channels are zero. :returns: The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. See From 044de40c93064aa938dc45868ce52698062009da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 11:28:14 +1000 Subject: [PATCH 663/727] Document that alpha_only is a keyword-only argument --- src/PIL/Image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4413bae288e..340ba4e3bb3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1301,6 +1301,7 @@ def getbbox(self, *, alpha_only=True): :param alpha_only: Optional flag, defaulting to ``True``. If ``True`` and the image has an alpha channel, trim transparent pixels. Otherwise, trim pixels when all channels are zero. + Keyword-only argument. :returns: The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. See :ref:`coordinate-system`. If the image is completely empty, this From 119a0dfb0113391bbab34fcd7be94457877506cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 11:29:22 +1000 Subject: [PATCH 664/727] Updated tests now that alpha_only is keyword-only --- Tests/test_image_getbbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index eec97821098..afca6670305 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -45,11 +45,11 @@ def check(im, fill_color): @pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) def test_bbox_alpha_only_false(mode): im = Image.new(mode, (100, 100)) - assert im.getbbox(False) is None + assert im.getbbox(alpha_only=False) is None fill_color = [1] * Image.getmodebands(mode) fill_color[-1] = 0 im.paste(tuple(fill_color), (25, 25, 75, 75)) - assert im.getbbox(False) == (25, 25, 75, 75) + assert im.getbbox(alpha_only=False) == (25, 25, 75, 75) assert im.getbbox() is None From 541d2605b9235a427379e3df51c9cdd4ffe59998 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 14:21:07 +1000 Subject: [PATCH 665/727] Allow alpha differences to indicate different frames when saving GIF --- Tests/test_file_gif.py | 12 ++++++++++++ src/PIL/GifImagePlugin.py | 4 ++-- src/PIL/PngImagePlugin.py | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0e50ee1abf9..f4a17264f4a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1130,6 +1130,18 @@ def test_bbox(tmp_path): assert reread.n_frames == 2 +def test_bbox_alpha(tmp_path): + out = str(tmp_path / "temp.gif") + + im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) + im.putpixel((0, 1), (255, 0, 0, 0)) + im2 = Image.new("RGBA", (1, 2), (255, 0, 0, 0)) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reread: + assert reread.n_frames == 2 + + def test_palette_save_L(tmp_path): # Generate an L mode image with a separate palette diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2f92e946751..cf2993e3892 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -569,9 +569,9 @@ def _getbbox(base_im, im_frame): delta = ImageChops.subtract_modulo(im_frame, base_im) else: delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") + im_frame.convert("RGBA"), base_im.convert("RGBA") ) - return delta.getbbox() + return delta.getbbox(alpha_only=False) def _write_multiple_frames(im, fp, palette): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index ea3052fbf7c..bfa8cb7ac66 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1140,7 +1140,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) delta = ImageChops.subtract_modulo( im_frame.convert("RGBA"), base_im.convert("RGBA") ) - bbox = delta.im.getbbox(False) + bbox = delta.getbbox(alpha_only=False) if ( not bbox and prev_disposal == encoderinfo.get("disposal") From 38d63868bf395ac1eb4869bc415de424f0863e45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 15:46:24 +1000 Subject: [PATCH 666/727] Do not import internal class --- _custom_build/backend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 86fe6081733..9b3265a949f 100755 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -1,10 +1,12 @@ import sys from setuptools.build_meta import * # noqa: F401, F403 -from setuptools.build_meta import _BuildMetaBackend +from setuptools.build_meta import build_wheel +backend_class = build_wheel.__self__.__class__ -class _CustomBuildMetaBackend(_BuildMetaBackend): + +class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: From 7d97fa8b86a9f61069579fbedb2cc09fb437b12f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 16:12:47 +1000 Subject: [PATCH 667/727] Use snake case --- Tests/test_imageops.py | 4 ++-- src/PIL/ImageOps.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index e7d04cceb3b..b05785be0ec 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -404,13 +404,13 @@ def check(orientation_im): assert 0x0112 not in transposed_im.getexif() -def test_exif_transpose_inplace(): +def test_exif_transpose_in_place(): with Image.open("Tests/images/orientation_rectangle.jpg") as im: assert im.size == (2, 1) assert im.getexif()[0x0112] == 8 expected = im.rotate(90, expand=True) - ImageOps.exif_transpose(im, inPlace=True) + ImageOps.exif_transpose(im, in_place=True) assert im.size == (1, 2) assert 0x0112 not in im.getexif() assert_image_equal(im, expected) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 752d132a320..17702778c13 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -576,13 +576,13 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, inPlace=False): +def exif_transpose(image, *, in_place=False): """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. :param image: The image to transpose. - :param inPlace: Boolean. Keyword-only argument. + :param in_place: Boolean. Keyword-only argument. If ``True``, the original image is modified in-place, and ``None`` is returned. If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned with the transposition applied. If there is no transposition, a copy of the @@ -601,11 +601,11 @@ def exif_transpose(image, *, inPlace=False): }.get(orientation) if method is not None: transposed_image = image.transpose(method) - if inPlace: + if in_place: image.im = transposed_image.im image.pyaccess = None image._size = transposed_image._size - exif_image = image if inPlace else transposed_image + exif_image = image if in_place else transposed_image exif = exif_image.getexif() if ExifTags.Base.Orientation in exif: @@ -622,7 +622,7 @@ def exif_transpose(image, *, inPlace=False): exif_image.info["XML:com.adobe.xmp"] = re.sub( pattern, "", exif_image.info["XML:com.adobe.xmp"] ) - if not inPlace: + if not in_place: return transposed_image - elif not inPlace: + elif not in_place: return image.copy() From b2b05f3b83ade2065b98d310214e6f49f04f57f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 18:55:19 +1000 Subject: [PATCH 668/727] Moved QOI from Write-Only to Read-Only --- docs/handbook/image-file-formats.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 74ba883b15e..bbcf48e4260 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1380,6 +1380,12 @@ PSD Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow identifies and reads images in Quite OK Image format. SUN ^^^ @@ -1562,13 +1568,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 5.3.0 -QOI -^^^ - -.. versionadded:: 9.5.0 - -Pillow identifies and reads images in Quite OK Image format. - XV Thumbnails ^^^^^^^^^^^^^ From 594fbf79b8a30a9fd3be1171038b5d787f4d00cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 23:01:45 +1000 Subject: [PATCH 669/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c51f8fb947c..4af3fa51634 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added in_place argument to ImageOps.exif_transpose() #7092 + [radarhere] + +- Fixed calling putpalette() on L and LA images before load() #7187 + [radarhere] + +- Fixed saving TIFF multiframe images with LONG8 tag types #7078 + [radarhere] + - Fixed combining single duration across duplicate APNG frames #7146 [radarhere] From 618c00c4ea64ddd40f3485db549f1549f7f27b04 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Jun 2023 14:27:33 +1000 Subject: [PATCH 670/727] Return early if image is null --- src/_imagingft.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index dbea673f975..d4422a43de3 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -887,6 +887,10 @@ font_render(FontObject *self, PyObject *args) { } image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + if (image == NULL) { + PyMem_Del(glyph_info); + return NULL; + } id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); im = (Imaging)id; From 98cc2e63ac9c78e57476d563a70a27397fea87e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Jun 2023 12:52:57 +1000 Subject: [PATCH 671/727] Destroy image on error --- src/_imagingft.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d4422a43de3..02d54fe2378 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -904,7 +904,8 @@ font_render(FontObject *self, PyObject *args) { if (stroke_width) { error = FT_Stroker_New(library, &stroker); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } FT_Stroker_Set( @@ -927,7 +928,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } glyph_slot = self->face->glyph; @@ -958,7 +960,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } glyph_slot = self->face->glyph; @@ -972,7 +975,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); } if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } bitmap_glyph = (FT_BitmapGlyph)glyph; @@ -1114,6 +1118,12 @@ font_render(FontObject *self, PyObject *args) { return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); glyph_error: + if (im->destroy) { + im->destroy(im); + } + if (im->image) { + free(im->image); + } if (stroker != NULL) { FT_Done_Glyph(glyph); } From 43b693972a4b2f5ffe00ecb21ec9cc46ab7a3352 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 00:25:59 +1000 Subject: [PATCH 672/727] Added PyPy 3.10 and removed PyPy 3.8 --- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 076b8083966..cab47b01f3d 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -28,10 +28,10 @@ jobs: architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows - - python-version: "pypy3.8" - architecture: "x64" - python-version: "pypy3.9" architecture: "x64" + - python-version: "pypy3.10" + architecture: "x64" timeout-minutes: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index afb8fb56c77..893c0d12c6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,8 +29,8 @@ jobs: "ubuntu-latest", ] python-version: [ + "pypy3.10", "pypy3.9", - "pypy3.8", "3.12-dev", "3.11", "3.10", From 7044038e701fd777bf2c7dc38b02c4d944b086ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 14:35:44 +1000 Subject: [PATCH 673/727] Fixed decompression bomb check --- ...om-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf | Bin 0 -> 30 bytes Tests/test_imagefont.py | 1 + src/_imagingft.c | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf diff --git a/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fe200842e41daa66ddb9a3ea5137726681bc2d2f GIT binary patch literal 30 kcmZQ%U}NO`&A`CG$jHcNpjXjl(8$C7=N4O!AW)0}08Ie}y8r+H literal 0 HcmV?d00001 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4a40d1d1d85..7fa8ff8cbfd 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1042,6 +1042,7 @@ def test_render_mono_size(): "test_file", [ "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) def test_oom(test_file): diff --git a/src/_imagingft.c b/src/_imagingft.c index 02d54fe2378..d421e5a0ba2 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { width += stroke_width * 2 + ceil(x_start); height += stroke_width * 2 + ceil(y_start); if (max_image_pixels != Py_None) { - if (width * height > PyLong_AsLong(max_image_pixels) * 2) { + if ((long long)width * height > PyLong_AsLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); } From fd9bea271a521e1ce054b13c723e9e47a759187e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 17 Jun 2023 14:39:34 +1000 Subject: [PATCH 674/727] Compare long long with long long MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d421e5a0ba2..6cee021d471 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { width += stroke_width * 2 + ceil(x_start); height += stroke_width * 2 + ceil(y_start); if (max_image_pixels != Py_None) { - if ((long long)width * height > PyLong_AsLong(max_image_pixels) * 2) { + if ((long long)width * height > PyLong_AsLongLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); } From f72dd8576ee77adec988dc1fe9777ee25f581a05 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sat, 17 Jun 2023 12:55:58 +0200 Subject: [PATCH 675/727] Changed `grabclipboard()` to use PNG compression on macOS Before, a lossy JPG compression was used. --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 39ecdf42096..4de5c69fb2f 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -102,7 +102,7 @@ def grabclipboard(): + filepath + '" with write permission)', "try", - " write (the clipboard as JPEG picture) to theFile", + " write (the clipboard as «class PNGf») to theFile", "end try", "close access theFile", ] From 3c4ccdcff54a7890176e57b5daf6757891678110 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sat, 17 Jun 2023 12:59:42 +0200 Subject: [PATCH 676/727] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4af3fa51634..5da3986ba25 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 + [abey79] + - Added in_place argument to ImageOps.exif_transpose() #7092 [radarhere] From e52fa8fe386d5503bd3abb64773be208edcc58ca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 21:01:52 +1000 Subject: [PATCH 677/727] Use relevant extension for temporary file --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 4de5c69fb2f..927033c6073 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -95,7 +95,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N def grabclipboard(): if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".jpg") + fh, filepath = tempfile.mkstemp(".png") os.close(fh) commands = [ 'set theFile to (open for access POSIX file "' From 0440df0d83a7bfd92df130893432830e7d9b4183 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Jun 2023 20:14:23 +1000 Subject: [PATCH 678/727] Clarify that the changelog should not be updated in PRs [ci skip] --- .github/CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ba2b7d8ed26..d03fcf0d9da 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,6 +19,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Follow PEP 8. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. +- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged. ## Reporting Issues From f28ecc5808ba1c562ebf7c557d7817f3fe92823d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Jun 2023 17:45:24 +1000 Subject: [PATCH 679/727] Document how to install on MinGW when setuptools >= 60 --- docs/installation.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index ac54b037df3..dc1cd8653d6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -312,6 +312,11 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libraqm + https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with + MSYS2. To workaround this, before installing Pillow you must run:: + + export SETUPTOOLS_USE_DISTUTILS=stdlib + .. tab:: FreeBSD .. Note:: Only FreeBSD 10 and 11 tested From cb8956fffb1bc0df9c74b2b789c716c64dd065b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Jun 2023 23:27:45 +1000 Subject: [PATCH 680/727] Convert to HSV if mode is HSV in getcolor() --- Tests/test_imagecolor.py | 4 ++++ src/PIL/Image.py | 2 +- src/PIL/ImageColor.py | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index dcc44e6e342..2fae6151cfd 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -193,6 +193,10 @@ def test_rounding_errors(): Image.new("LA", (1, 1), "white") +def test_color_hsv(): + assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") + + def test_color_too_long(): # Arrange color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a785292f8f8..7b31178e308 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2882,7 +2882,7 @@ def new(mode, size, color=0): :param color: What color to use for the image. Default is black. If given, this should be a single integer or floating point value for single-band modes, and a tuple for multi-band modes (one value - per band). When creating RGB images, you can also use color + per band). When creating RGB or HSV images, you can also use color strings as supported by the ImageColor module. If the color is None, the image is not initialised. :returns: An :py:class:`~PIL.Image.Image` object. diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index e184ed68da3..befc1fd1d88 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -122,9 +122,11 @@ def getrgb(color): def getcolor(color, mode): """ - Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a - greyscale value if ``mode`` is not color or a palette image. If the string - cannot be parsed, this function raises a :py:exc:`ValueError` exception. + Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if + ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is + not color or a palette image, converts the RGB value to a greyscale value. + If the string cannot be parsed, this function raises a :py:exc:`ValueError` + exception. .. versionadded:: 1.1.4 @@ -137,7 +139,13 @@ def getcolor(color, mode): if len(color) == 4: color, alpha = color[:3], color[3] - if Image.getmodebase(mode) == "L": + if mode == "HSV": + from colorsys import rgb_to_hsv + + r, g, b = color + h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) + return int(h * 255), int(s * 255), int(v * 255) + elif Image.getmodebase(mode) == "L": r, g, b = color # ITU-R Recommendation 601-2 for nonlinear RGB # scaled to 24 bits to match the convert's implementation. From 56a795c8ddaa203ebb3bed6df99c96d60b366199 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 22 Jun 2023 09:05:03 -0500 Subject: [PATCH 681/727] add units to bench_cffi_access.py output --- Tests/bench_cffi_access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 87cad699d3b..49ff349491d 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -28,7 +28,7 @@ def timer(func, label, *args): func(*args) if time.time() - starttime > 10: print( - "{}: breaking at {} iterations, {:.6f} per iteration".format( + "{}: breaking at {} iterations, {:.6f}s per iteration".format( label, x + 1, (time.time() - starttime) / (x + 1.0) ) ) @@ -36,7 +36,7 @@ def timer(func, label, *args): if x == iterations - 1: endtime = time.time() print( - "{}: {:.4f} s {:.6f} per iteration".format( + "{}: {:.4f}s total, {:.6f}s per iteration".format( label, endtime - starttime, (endtime - starttime) / (x + 1.0) ) ) From ff4c7ffceaa1a67d036375d44020702347c65cbf Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 22 Jun 2023 09:16:18 -0500 Subject: [PATCH 682/727] use same print format regardless of iterations --- Tests/bench_cffi_access.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 49ff349491d..69ebef9b458 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -27,25 +27,19 @@ def timer(func, label, *args): for x in range(iterations): func(*args) if time.time() - starttime > 10: - print( - "{}: breaking at {} iterations, {:.6f}s per iteration".format( - label, x + 1, (time.time() - starttime) / (x + 1.0) - ) - ) break - if x == iterations - 1: - endtime = time.time() - print( - "{}: {:.4f}s total, {:.6f}s per iteration".format( - label, endtime - starttime, (endtime - starttime) / (x + 1.0) - ) + endtime = time.time() + print( + "{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format( + label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0) ) + ) def test_direct(): im = hopper() im.load() - # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) + # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) caccess = im.im.pixel_access(False) access = PyAccess.new(im, False) From d6f19625e8cadc92e792f3567c9e82ee057653fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Jun 2023 21:52:00 +1000 Subject: [PATCH 683/727] Removed support for 32-bit --- .appveyor.yml | 4 +- .github/workflows/test-docker.yml | 4 +- .github/workflows/test-mingw.yml | 63 +++++++++--------------------- .github/workflows/test-windows.yml | 18 +++------ Tests/32bit_segfault_check.py | 8 ---- Tests/check_large_memory.py | 5 --- Tests/check_large_memory_numpy.py | 5 --- Tests/test_core_resources.py | 5 --- Tests/test_file_webp.py | 2 - Tests/test_image_putdata.py | 5 +-- Tests/test_map.py | 3 -- docs/installation.rst | 17 +++----- setup.py | 20 ++++------ src/libImaging/ImagingUtils.h | 2 +- winbuild/build.rst | 6 +-- winbuild/build_prepare.py | 8 +--- 16 files changed, 47 insertions(+), 128 deletions(-) delete mode 100755 Tests/32bit_segfault_check.py diff --git a/.appveyor.yml b/.appveyor.yml index 36f5bd0ad68..9a2eef76781 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,8 +10,8 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python311 - ARCHITECTURE: x86 + - PYTHON: C:/Python311-x64 + ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 ARCHITECTURE: x64 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 3bcb8cfbca7..f22733dc463 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -38,8 +38,8 @@ jobs: centos-7-amd64, centos-stream-8-amd64, centos-stream-9-amd64, - debian-11-bullseye-x86, - debian-12-bookworm-x86, + debian-11-bullseye-amd64, + debian-12-bookworm-amd64, fedora-37-amd64, fedora-38-amd64, gentoo, diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a109ec0d8b5..4269eeb62bb 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -21,27 +21,16 @@ concurrency: jobs: build: runs-on: windows-latest - strategy: - fail-fast: false - matrix: - mingw: ["MINGW32", "MINGW64"] - include: - - mingw: "MINGW32" - name: "MSYS2 MinGW 32-bit" - package: "mingw-w64-i686" - - mingw: "MINGW64" - name: "MSYS2 MinGW 64-bit" - package: "mingw-w64-x86_64" defaults: run: shell: bash.exe --login -eo pipefail "{0}" env: - MSYSTEM: ${{ matrix.mingw }} + MSYSTEM: MINGW64 CHERE_INVOKING: 1 timeout-minutes: 30 - name: ${{ matrix.name }} + name: "MSYS2 MinGW" steps: - name: Checkout Pillow @@ -54,26 +43,22 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-freetype \ - ${{ matrix.package }}-gcc \ - ${{ matrix.package }}-ghostscript \ - ${{ matrix.package }}-lcms2 \ - ${{ matrix.package }}-libimagequant \ - ${{ matrix.package }}-libjpeg-turbo \ - ${{ matrix.package }}-libraqm \ - ${{ matrix.package }}-libtiff \ - ${{ matrix.package }}-libwebp \ - ${{ matrix.package }}-openjpeg2 \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools - - if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then - pacman -S --noconfirm \ - ${{ matrix.package }}-python-pyqt6 - fi + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-ghostscript \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-python3-cffi \ + mingw-w64-x86_64-python3-numpy \ + mingw-w64-x86_64-python3-olefile \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools \ + mingw-w64-x86_64-python-pyqt6 python3 -m pip install pyroma pytest pytest-cov pytest-timeout @@ -93,14 +78,4 @@ jobs: with: file: ./coverage.xml flags: GHA_Windows - name: ${{ matrix.name }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: MinGW Test Successful - steps: - - name: Success - run: echo MinGW Test Successful + name: "MSYS2 MinGW" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cab47b01f3d..b5fd4395f4c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -24,18 +24,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev"] - architecture: ["x86", "x64"] - include: - # PyPy 7.3.4+ only ships 64-bit binaries for Windows - - python-version: "pypy3.9" - architecture: "x64" - - python-version: "pypy3.10" - architecture: "x64" + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev", "pypy3.9", "pypy3.10"] timeout-minutes: 30 - name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} + name: Python ${{ matrix.python-version }} steps: - name: Checkout Pillow @@ -58,7 +51,6 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.architecture }} cache: pip cache-dependency-path: ".github/workflows/test-windows.yml" @@ -206,14 +198,14 @@ jobs: with: file: ./coverage.xml flags: GHA_Windows - name: ${{ runner.os }} Python ${{ matrix.python-version }} ${{ matrix.architecture }} + name: ${{ runner.os }} Python ${{ matrix.python-version }} - name: Build wheel id: wheel if: "github.event_name != 'pull_request'" run: | - mkdir fribidi\${{ matrix.architecture }} - copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }} + mkdir fribidi + copy winbuild\build\bin\fribidi* fribidi setlocal EnableDelayedExpansion for %%f in (winbuild\build\license\*) do ( set x=%%~nf diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py deleted file mode 100755 index 2ff7f908f6d..00000000000 --- a/Tests/32bit_segfault_check.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -from PIL import Image - -if sys.maxsize < 2**32: - im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index d98f4a694ef..219788d7bd9 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,5 +1,3 @@ -import sys - import pytest from PIL import Image @@ -23,9 +21,6 @@ XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") - - def _write_png(tmp_path, xdim, ydim): f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 24cb1f722bf..c54894721d0 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,5 +1,3 @@ -import sys - import pytest from PIL import Image @@ -19,9 +17,6 @@ XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") - - def _write_png(tmp_path, xdim, ydim): dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 9021a9fb36d..f2105d6ca6c 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,5 +1,3 @@ -import sys - import pytest from PIL import Image @@ -110,9 +108,6 @@ def test_set_blocks_max(self): with pytest.raises(ValueError): Image.core.set_blocks_max(-1) - if sys.maxsize < 2**32: - with pytest.raises(ValueError): - Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index a7b6c735a69..01b11447ac4 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,6 +1,5 @@ import io import re -import sys import warnings import pytest @@ -145,7 +144,6 @@ def test_write_unsupported_mode_P(self, tmp_path): self._roundtrip(tmp_path, "P", 50.0) - @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path): temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 0e6293349bc..db5307d2ccb 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -38,10 +38,7 @@ def put(value): assert put(0xFFFFFFFF) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) - if sys.maxsize > 2**32: - assert put(sys.maxsize) == (255, 255, 255, 255) - else: - assert put(sys.maxsize) == (255, 255, 255, 127) + assert put(sys.maxsize) == (255, 255, 255, 255) def test_pypy_performance(): diff --git a/Tests/test_map.py b/Tests/test_map.py index d816bddaf3d..42b6f7cddab 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,5 +1,3 @@ -import sys - import pytest from PIL import Image @@ -36,7 +34,6 @@ def test_tobytes(): Image.MAX_IMAGE_PIXELS = max_pixels -@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") diff --git a/docs/installation.rst b/docs/installation.rst index dc1cd8653d6..79e14d47815 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -285,11 +285,8 @@ Many of Pillow's features require external libraries: .. tab:: Windows using MSYS2/MinGW - To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or - **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. - - The following instructions target the 64-bit build, for 32-bit - replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 64-bit** console, + *not* **MSYS2** directly. Make sure you have Python and GCC installed:: @@ -339,8 +336,6 @@ Many of Pillow's features require external libraries: pkg install -y python ndk-sysroot clang make \ libjpeg-turbo - This has been tested within the Termux app on ChromeOS, on x86. - Installing ^^^^^^^^^^ @@ -451,9 +446,9 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 11 Bullseye | 3.9 | x86 | +| Debian 11 Bullseye | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 12 Bookworm | 3.11 | x86 | +| Debian 12 Bookworm | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -474,10 +469,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | +| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86, x86-64 | +| | 3.9 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.8, 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ diff --git a/setup.py b/setup.py index 7c1ad6dc5b2..b647944d3f7 100755 --- a/setup.py +++ b/setup.py @@ -153,16 +153,13 @@ def _find_library_dirs_ldconfig(): ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): - if struct.calcsize("l") == 4: - machine = os.uname()[4] + "-32" - else: - machine = os.uname()[4] + "-64" + machine = os.uname()[4] mach_map = { - "x86_64-64": "libc6,x86-64", - "ppc64-64": "libc6,64bit", - "sparc64-64": "libc6,64bit", - "s390x-64": "libc6,64bit", - "ia64-64": "libc6,IA-64", + "x86_64": "libc6,x86-64", + "ppc64": "libc6,64bit", + "sparc64": "libc6,64bit", + "s390x": "libc6,64bit", + "ia64": "libc6,IA-64", } abi_type = mach_map.get(machine, "libc6") @@ -584,10 +581,7 @@ def build_extensions(self): # user libs are at $PREFIX/lib _add_directory( library_dirs, - os.path.join( - os.environ["ANDROID_ROOT"], - "lib" if struct.calcsize("l") == 4 else "lib64", - ), + os.path.join(os.environ["ANDROID_ROOT"], "lib64"), ) elif sys.platform.startswith("netbsd"): diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index 0c0c1eda917..f2acabeac1a 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -30,7 +30,7 @@ /* This is to work around a bug in GCC prior 4.9 in 64 bit mode. GCC generates code with partial dependency which is 3 times slower. See: https://stackoverflow.com/a/26588074/253146 */ -#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \ +#if defined(__SSE__) && !defined(__NO_INLINE__) && \ !defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) static float __attribute__((always_inline)) inline _i2f(int v) { float x; diff --git a/winbuild/build.rst b/winbuild/build.rst index 99dfad3015f..97df950b348 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -27,7 +27,7 @@ Download and install: * `Ninja `_ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) -* x86/x64: `Netwide Assembler (NASM) `_ +* x64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x86,x64,ARM64}] + [--architecture {x64,ARM64}] [--python PYTHON] [--executable EXECUTABLE] [--nmake] [--no-imagequant] [--no-fribidi] @@ -56,7 +56,7 @@ Run ``build_prepare.py`` to configure the build:: --depends PILLOW_DEPS directory used to store cached dependencies (default: 'winbuild\depends') - --architecture {x86,x64,ARM64} + --architecture {x64,ARM64} build architecture (default: same as host Python) --python PYTHON Python install directory (default: use host Python) --executable EXECUTABLE diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 89b2daad005..d8212ee5150 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -3,7 +3,6 @@ import platform import re import shutil -import struct import subprocess import sys @@ -98,7 +97,6 @@ def cmd_msbuild( SF_PROJECTS = "https://sourceforge.net/projects" architectures = { - "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -611,11 +609,7 @@ def build_pillow(): choices=architectures, default=os.environ.get( "ARCHITECTURE", - ( - "ARM64" - if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64") - ), + "ARM64" if platform.machine() == "ARM64" else "x64", ), help="build architecture (default: same as host Python)", ) From 43a12542ad83c8da7aa138fbf376b22895775e37 Mon Sep 17 00:00:00 2001 From: Rozie <60040522+RoziePlaysPython@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:05:28 +0300 Subject: [PATCH 684/727] Update Image.show docs to list all viewers used on linux [ci skip] Accurate description of how default viewer is chosen --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a785292f8f8..c19bdcac820 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2451,7 +2451,7 @@ def show(self, title=None): The image is first saved to a temporary file. By default, it will be in PNG format. - On Unix, the image is then opened using the **display**, **eog** or + On Unix, the image is then opened using the **xdg-open**, **display**, **gm**, **eog** or **xv** utility, depending on which one can be found. On macOS, the image is opened with the native Preview application. From b0b079882047ca2853714ba899ef719cc2ed70f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 23 Jun 2023 22:22:33 +1000 Subject: [PATCH 685/727] Lint fix --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c19bdcac820..97f3f492624 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2451,8 +2451,8 @@ def show(self, title=None): The image is first saved to a temporary file. By default, it will be in PNG format. - On Unix, the image is then opened using the **xdg-open**, **display**, **gm**, **eog** or - **xv** utility, depending on which one can be found. + On Unix, the image is then opened using the **xdg-open**, **display**, + **gm**, **eog** or **xv** utility, depending on which one can be found. On macOS, the image is opened with the native Preview application. From 6c464b810185494df6cd1c64ed21ae455ebc93c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 13:09:07 +1000 Subject: [PATCH 686/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5da3986ba25..c052687725e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,11 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed finding dependencies on Cygwin #7175 + [radarhere] + - Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 - [abey79] + [abey79, radarhere] - Added in_place argument to ImageOps.exif_transpose() #7092 [radarhere] From 5498cb800c8a432f709cffde219d38ef521fd4e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:46:42 +1000 Subject: [PATCH 687/727] Order slower jobs first Co-authored-by: Hugo van Kemenade --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b5fd4395f4c..3a24fd36a3e 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev", "pypy3.9", "pypy3.10"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] timeout-minutes: 30 From 9c5175d04871647dfae3a3286b9b60d250a0566f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 17:52:28 +1000 Subject: [PATCH 688/727] Added release notes --- docs/releasenotes/10.0.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 3ffafafdc15..ec3a6927efb 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -4,6 +4,11 @@ Backwards Incompatible Changes ============================== +32-bit support +^^^^^^^^^^^^^^ + +32-bit architecture is no longer supported. + Categories ^^^^^^^^^^ From b6751b24de19770da2b18ebd112ef069c7e741dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 19:17:30 +1000 Subject: [PATCH 689/727] Updated mergify --- .github/mergify.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/mergify.yml b/.github/mergify.yml index 8dfa07f4ec5..3c20661376f 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -7,7 +7,7 @@ pull_request_rules: - status-success=Test Successful - status-success=Docker Test Successful - status-success=Windows Test Successful - - status-success=MinGW Test Successful + - status-success=MinGW - status-success=Cygwin Test Successful - status-success=continuous-integration/appveyor/pr actions: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 4269eeb62bb..59fd5ed9dc7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -30,7 +30,7 @@ jobs: CHERE_INVOKING: 1 timeout-minutes: 30 - name: "MSYS2 MinGW" + name: "MinGW" steps: - name: Checkout Pillow From abf05414de5eecf5b3115a86e72247adc6ceaebd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 21:52:13 +1000 Subject: [PATCH 690/727] Mention that 32-bit wheels are no longer provided --- docs/releasenotes/10.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index ec3a6927efb..d33b75e4da3 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -7,7 +7,7 @@ Backwards Incompatible Changes 32-bit support ^^^^^^^^^^^^^^ -32-bit architecture is no longer supported. +32-bit architecture is no longer supported and 32-bit wheels are no longer provided. Categories ^^^^^^^^^^ From 83867f5c35fe239d5fefd15081d74ac480be5a52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 22:08:52 +1000 Subject: [PATCH 691/727] Updated freetype to 2.13.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 89b2daad005..b9de071a0a5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -237,9 +237,9 @@ def cmd_msbuild( "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501 - "filename": "freetype-2.13.0.tar.gz", - "dir": "freetype-2.13.0", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.1.tar.gz", # noqa: E501 + "filename": "freetype-2.13.1.tar.gz", + "dir": "freetype-2.13.1", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { From 5d0e37e255aca3aabb97a619ca46e2f9bdb4f3a8 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 24 Jun 2023 15:13:26 +0100 Subject: [PATCH 692/727] use --config-settings when building on Windows --- .appveyor.yml | 16 ++--- .github/workflows/test-windows.yml | 13 ++-- winbuild/build_prepare.py | 102 +++++++++-------------------- 3 files changed, 44 insertions(+), 87 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 575b6caa603..60132a9a35a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -38,10 +38,9 @@ install: - path C:\pillow\winbuild\build\bin;%PATH% build_script: -- ps: | - c:\pillow\winbuild\build\build_pillow.cmd install - $host.SetShouldExit(0) - cd c:\pillow +- winbuild\build\build_env.cmd +- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .' - '%PYTHON%\%EXECUTABLE% selftest.py --installed' test_script: @@ -63,18 +62,15 @@ cache: - '%LOCALAPPDATA%\pip\Cache' artifacts: -- path: pillow\dist\*.egg +- path: pillow\*.egg name: egg -- path: pillow\dist\*.wheel +- path: pillow\*.whl name: wheel before_deploy: - cd c:\pillow - - '%PYTHON%\%EXECUTABLE% -m pip install wheel' - - cd c:\pillow\winbuild\ - - c:\pillow\winbuild\build\build_pillow.cmd --global-option="bdist_wheel" - - cd c:\pillow - - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .' + - ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } deploy: provider: S3 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 3bfd54ee31b..821e037a27f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -97,7 +97,7 @@ jobs: - name: Prepare build if: steps.build-cache.outputs.cache-hit != 'true' run: | - & python.exe winbuild\build_prepare.py -v --python $env:pythonLocation + & python.exe winbuild\build_prepare.py -v shell: pwsh - name: Build dependencies / libjpeg-turbo @@ -165,9 +165,9 @@ jobs: - name: Build Pillow run: | - $FLAGS="" - if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS='--global-option="--disable-imagequant"' } - & winbuild\build\build_pillow.cmd $FLAGS --global-option="install" + $FLAGS="-C raqm=vendor -C fribidi=vendor" + if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" } + cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -231,7 +231,8 @@ jobs: ) ) for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% - winbuild\\build\\build_pillow.cmd --global-option="--disable-imagequant" --global-option="bdist_wheel" + call winbuild\\build\\build_env.cmd + %pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable . shell: cmd - name: Upload wheel @@ -239,7 +240,7 @@ jobs: if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} - path: dist\*.whl + path: "*.whl" - name: Upload fribidi.dll if: "github.event_name != 'pull_request' && matrix.python-version == 3.11" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b41a735dbe4..e7ccf3385ff 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -103,13 +103,6 @@ def cmd_msbuild( "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } -header = [ - cmd_set("INCLUDE", "{inc_dir}"), - cmd_set("INCLIB", "{lib_dir}"), - cmd_set("LIB", "{lib_dir}"), - cmd_append("PATH", "{bin_dir}"), -] - # dependencies, listed in order of compilation deps = { "libjpeg": { @@ -401,23 +394,12 @@ def find_msvs(): print("Visual Studio seems to be missing C compiler") return None - vs = { - "header": [], - # nmake selected by vcvarsall - "nmake": "nmake.exe", - "vs_dir": vspath, - } - # vs2017 msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe") - if os.path.isfile(msbuild): - vs["msbuild"] = f'"{msbuild}"' - else: + if not os.path.isfile(msbuild): # vs2019 msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe") - if os.path.isfile(msbuild): - vs["msbuild"] = f'"{msbuild}"' - else: + if not os.path.isfile(msbuild): print("Visual Studio MSBuild not found") return None @@ -425,9 +407,13 @@ def find_msvs(): if not os.path.isfile(vcvarsall): print("Visual Studio vcvarsall not found") return None - vs["header"].append(f'call "{vcvarsall}" {{vcvars_arch}}') - return vs + return { + "vs_dir": vspath, + "msbuild": f'"{msbuild}"', + "vcvarsall": f'"{vcvarsall}"', + "nmake": "nmake.exe", # nmake selected by vcvarsall + } def extract_dep(url, filename): @@ -497,6 +483,22 @@ def get_footer(dep): return lines +def build_env(): + lines = [ + "if defined DISTUTILS_USE_SDK goto end", + cmd_set("INCLUDE", "{inc_dir}"), + cmd_set("INCLIB", "{lib_dir}"), + cmd_set("LIB", "{lib_dir}"), + cmd_append("PATH", "{bin_dir}"), + f"call {{vcvarsall}} {{vcvars_arch}}", + cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow + cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT + ":end", + "@echo on", + ] + write_script("build_env.cmd", lines) + + def build_dep(name): dep = deps[name] dir = dep["dir"] @@ -534,11 +536,11 @@ def build_dep(name): banner = f"Building {name} ({dir})" lines = [ + rf'call "{{build_dir}}\build_env.cmd"', "@echo " + ("=" * 70), f"@echo ==== {banner:<60} ====", "@echo " + ("=" * 70), - "cd /D %s" % os.path.join(sources_dir, dir), - *prefs["header"], + cmd_cd(os.path.join(sources_dir, dir)), *dep.get("build", []), *get_footer(dep), ] @@ -548,7 +550,7 @@ def build_dep(name): def build_dep_all(): - lines = ["@echo on"] + lines = [r'call "{build_dir}\build_env.cmd"'] for dep_name in deps: print() if dep_name in disabled: @@ -562,32 +564,16 @@ def build_dep_all(): write_script("build_dep_all.cmd", lines) -def build_pillow(): - lines = [ - "@echo ---- Building Pillow (build_ext %*) ----", - cmd_cd("{pillow_dir}"), - *prefs["header"], - cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow - cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT - r'"{python_dir}\{python_exe}" -m pip install . ' - r'--global-option="--vendor-raqm" ' - r'--global-option="--vendor-fribidi" ' - r"%*", - ] - - write_script("build_pillow.cmd", lines) - - if __name__ == "__main__": winbuild_dir = os.path.dirname(os.path.realpath(__file__)) pillow_dir = os.path.realpath(os.path.join(winbuild_dir, "..")) parser = argparse.ArgumentParser( prog="winbuild\\build_prepare.py", - description="Download dependencies and generate build scripts for Pillow.", + description="Download and generate build scripts for Pillow dependencies.", epilog="""Arguments can also be supplied using the environment variables - PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. - See winbuild\\build.rst for more information.""", + PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\\build.rst + for more information.""", ) parser.add_argument( "-v", "--verbose", action="store_true", help="print generated scripts" @@ -622,20 +608,6 @@ def build_pillow(): ), help="build architecture (default: same as host Python)", ) - parser.add_argument( - "--python", - dest="python_dir", - metavar="PYTHON", - default=os.environ.get("PYTHON"), - help="Python install directory (default: use host Python)", - ) - parser.add_argument( - "--executable", - dest="python_exe", - metavar="EXECUTABLE", - default=os.environ.get("EXECUTABLE", "python.exe"), - help="Python executable (default: use host Python)", - ) parser.add_argument( "--nmake", dest="cmake_generator", @@ -660,11 +632,6 @@ def build_pillow(): arch_prefs = architectures[args.architecture] print("Target architecture:", args.architecture) - if args.python_dir is None: - args.python_dir = os.path.dirname(os.path.realpath(sys.executable)) - args.python_exe = os.path.basename(sys.executable) - print("Target Python:", os.path.join(args.python_dir, args.python_exe)) - msvs = find_msvs() if msvs is None: msg = "Visual Studio not found. Please install Visual Studio 2017 or newer." @@ -702,9 +669,6 @@ def build_pillow(): disabled += ["fribidi"] prefs = { - # Python paths / preferences - "python_dir": args.python_dir, - "python_exe": args.python_exe, "architecture": args.architecture, **arch_prefs, # Pillow paths @@ -722,8 +686,6 @@ def build_pillow(): "cmake": "cmake.exe", # TODO find CMAKE automatically "cmake_generator": args.cmake_generator, # TODO find NASM automatically - # script header - "header": sum([header, msvs["header"], ["@echo on"]], []), } for k, v in deps.items(): @@ -732,7 +694,5 @@ def build_pillow(): print() write_script(".gitignore", ["*"]) + build_env() build_dep_all() - if args.verbose: - print() - build_pillow() From 466aa7e6c4cdbeab794c1e024b0e95e80a6bb1a2 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 24 Jun 2023 15:52:59 +0100 Subject: [PATCH 693/727] update winbuild documentation --- winbuild/README.md | 6 +++--- winbuild/build.rst | 36 +++++++++++++++++------------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 2975acf283f..7e81abcb0e5 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -18,12 +18,12 @@ The following is a simplified version of the script used on AppVeyor: ``` set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild -C:\Python39\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends +%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends build\build_dep_all.cmd -build\build_pillow.cmd install cd .. +%PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests -build\build_pillow.cmd bdist_wheel +%PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . ``` diff --git a/winbuild/build.rst b/winbuild/build.rst index 99dfad3015f..a8e4ebaa6cc 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -42,11 +42,10 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x86,x64,ARM64}] - [--python PYTHON] [--executable EXECUTABLE] - [--nmake] [--no-imagequant] [--no-fribidi] + [--architecture {x86,x64,ARM64}] [--nmake] + [--no-imagequant] [--no-fribidi] - Download dependencies and generate build scripts for Pillow. + Download and generate build scripts for Pillow dependencies. options: -h, --help show this help message and exit @@ -58,17 +57,13 @@ Run ``build_prepare.py`` to configure the build:: 'winbuild\depends') --architecture {x86,x64,ARM64} build architecture (default: same as host Python) - --python PYTHON Python install directory (default: use host Python) - --executable EXECUTABLE - Python executable (default: use host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant --no-fribidi, --no-raqm skip LGPL-licensed optional dependency FriBiDi Arguments can also be supplied using the environment variables PILLOW_BUILD, - PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. See winbuild\build.rst for more - information. + PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. **Warning:** The build directory is wiped when ``build_prepare.py`` is run. @@ -86,14 +81,16 @@ or run the individual scripts in order to build each dependency separately. Building Pillow --------------- -Once the dependencies are built, run -``winbuild\build\build_pillow.cmd install`` to build and install -Pillow for the selected version of Python. -``winbuild\build\build_pillow.cmd bdist_wheel`` will build wheels -instead of installing Pillow. +Once the dependencies are built, make sure the required environment variables +are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip:: -You can also use ``winbuild\build\build_pillow.cmd --inplace develop`` to build -and install Pillow in develop mode (instead of ``python3 -m pip install --editable``). + winbuild\build\build_env.cmd + python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . + +To build a wheel instead, run:: + + winbuild\build\build_env.cmd + python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . Testing Pillow -------------- @@ -112,11 +109,12 @@ The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild - C:\Python39\bin\python.exe build_prepare.py -v --depends C:\pillow-depends + %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd - build\build_pillow.cmd install + build\build_env.cmd cd .. + %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - build\build_pillow.cmd bdist_wheel + %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . From 14ae3e446cdd24c6304513b109e650cdc03a2d57 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 13:49:47 +1000 Subject: [PATCH 694/727] Updated pyproject-fmt to 0.12.1 (cherry picked from commit 0e1f86a425075db3a755db2e81502ec6eb6ba9e0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 79966503f1e..872c73843c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.11.2 + rev: 0.12.1 hooks: - id: pyproject-fmt From 25c24a8a9157d252e9edb468c70eea04e5b9e246 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 13:50:27 +1000 Subject: [PATCH 695/727] Removed unused code (cherry picked from commit 600b823de6acbe6c3920633af4c64f6db61565d4) --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index e2ed3b315c6..024634ad8f9 100755 --- a/setup.py +++ b/setup.py @@ -137,7 +137,6 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -PLATFORM_PYPY = hasattr(sys, "pypy_version_info") def _dbg(s, tp=None): From c1799627df23bcda120cb9c278fb3c292e2b235e Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 24 Jun 2023 15:57:43 +0100 Subject: [PATCH 696/727] lint fixes --- pyproject.toml | 9 +++++++-- winbuild/build_prepare.py | 5 ++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1d32e7d90ef..93a43360891 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,9 @@ [build-system] -requires = ["setuptools >= 67.8", "wheel"] build-backend = "backend" -backend-path = ["_custom_build"] +requires = [ + "setuptools>=67.8", + "wheel", +] +backend-path = [ + "_custom_build", +] diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e7ccf3385ff..fcc433942a7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -5,7 +5,6 @@ import shutil import struct import subprocess -import sys def cmd_cd(path): @@ -490,7 +489,7 @@ def build_env(): cmd_set("INCLIB", "{lib_dir}"), cmd_set("LIB", "{lib_dir}"), cmd_append("PATH", "{bin_dir}"), - f"call {{vcvarsall}} {{vcvars_arch}}", + "call {vcvarsall} {vcvars_arch}", cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT ":end", @@ -536,7 +535,7 @@ def build_dep(name): banner = f"Building {name} ({dir})" lines = [ - rf'call "{{build_dir}}\build_env.cmd"', + r'call "{build_dir}\build_env.cmd"', "@echo " + ("=" * 70), f"@echo ==== {banner:<60} ====", "@echo " + ("=" * 70), From 9c73834407677b774b104a25428a8320f44649ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 Jun 2023 13:33:20 +1000 Subject: [PATCH 697/727] Removed deleted file --- codecov.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index b794632faae..fe8c126edf8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -16,7 +16,6 @@ coverage: # Matches 'omit:' in .coveragerc ignore: - - "Tests/32bit_segfault_check.py" - "Tests/bench_cffi_access.py" - "Tests/check_*.py" - "Tests/createfontdatachunk.py" From 837d23f27386da5dd4b5b3812c041399e1c600e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 25 Jun 2023 13:58:02 +1000 Subject: [PATCH 698/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c052687725e..9d95f3cd257 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Removed support for 32-bit #7228 + [radarhere, hugovk] + +- Use --config-settings instead of deprecated --global-option #7171 + [radarhere] + +- Better C integer definitions #6645 + [Yay295, hugovk] + - Fixed finding dependencies on Cygwin #7175 [radarhere] From d12783374b3bdd40a156cc0e63b10aa17b5c1ab6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 Jun 2023 21:39:26 +1000 Subject: [PATCH 699/727] Increased coverage threshold --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index fe8c126edf8..40419979fb4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -12,7 +12,7 @@ coverage: status: project: default: - threshold: 0.01% + threshold: 0.1% # Matches 'omit:' in .coveragerc ignore: From fec793d8ab8bf2065ef0eb188966e4e2337c329e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sun, 25 Jun 2023 13:05:22 +0100 Subject: [PATCH 700/727] don't explicitly install wheel Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 20de322f600..70afbab24ee 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -57,8 +57,8 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install From 51cd79266dac9917c7a2442849041aae5368a820 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Jun 2023 13:51:17 +1000 Subject: [PATCH 701/727] Updated libtiff to 4.5.1 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index e29709bff0d..6262548d83e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -155,7 +155,7 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1** * **libfreetype** provides type related services diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9984eb1bccc..a9b6450d884 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -186,9 +186,9 @@ def cmd_msbuild( "libs": [r"output\release-static\{architecture}\lib\*.lib"], }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz", - "filename": "tiff-4.5.0.tar.gz", - "dir": "tiff-4.5.0", + "url": "https://download.osgeo.org/libtiff/tiff-4.5.1.tar.gz", + "filename": "tiff-4.5.1.tar.gz", + "dir": "tiff-4.5.1", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -199,6 +199,12 @@ def cmd_msbuild( # link against webp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 }, + r"test/CMakeLists.txt": { + "add_executable(test_write_read_tags ../placeholder.h)": "", + "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501 + "target_link_libraries(test_write_read_tags PRIVATE tiff)": "", + "list(APPEND simple_tests test_write_read_tags)": "", + }, }, "build": [ *cmds_cmake( From 45c9dcf123948069a6e28e39413b197ebf3549a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Jun 2023 14:43:58 +1000 Subject: [PATCH 702/727] Restored 32-bit support --- Tests/32bit_segfault_check.py | 8 ++++++++ Tests/check_large_memory.py | 5 +++++ Tests/check_large_memory_numpy.py | 5 +++++ Tests/test_core_resources.py | 5 +++++ Tests/test_file_webp.py | 2 ++ Tests/test_image_putdata.py | 5 ++++- Tests/test_map.py | 3 +++ codecov.yml | 1 + docs/installation.rst | 9 +++++++-- docs/releasenotes/10.0.0.rst | 18 +++++------------- setup.py | 20 +++++++++++++------- src/libImaging/ImagingUtils.h | 2 +- winbuild/build.rst | 6 +++--- winbuild/build_prepare.py | 8 +++++++- 14 files changed, 69 insertions(+), 28 deletions(-) create mode 100755 Tests/32bit_segfault_check.py diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py new file mode 100755 index 00000000000..2ff7f908f6d --- /dev/null +++ b/Tests/32bit_segfault_check.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +import sys + +from PIL import Image + +if sys.maxsize < 2**32: + im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 219788d7bd9..d98f4a694ef 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,3 +1,5 @@ +import sys + import pytest from PIL import Image @@ -21,6 +23,9 @@ XDIM = 48000 +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") + + def _write_png(tmp_path, xdim, ydim): f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index c54894721d0..24cb1f722bf 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,3 +1,5 @@ +import sys + import pytest from PIL import Image @@ -17,6 +19,9 @@ XDIM = 48000 +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") + + def _write_png(tmp_path, xdim, ydim): dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index f2105d6ca6c..9021a9fb36d 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,3 +1,5 @@ +import sys + import pytest from PIL import Image @@ -108,6 +110,9 @@ def test_set_blocks_max(self): with pytest.raises(ValueError): Image.core.set_blocks_max(-1) + if sys.maxsize < 2**32: + with pytest.raises(ValueError): + Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 01b11447ac4..a7b6c735a69 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,5 +1,6 @@ import io import re +import sys import warnings import pytest @@ -144,6 +145,7 @@ def test_write_unsupported_mode_P(self, tmp_path): self._roundtrip(tmp_path, "P", 50.0) + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path): temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index db5307d2ccb..0e6293349bc 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -38,7 +38,10 @@ def put(value): assert put(0xFFFFFFFF) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) - assert put(sys.maxsize) == (255, 255, 255, 255) + if sys.maxsize > 2**32: + assert put(sys.maxsize) == (255, 255, 255, 255) + else: + assert put(sys.maxsize) == (255, 255, 255, 127) def test_pypy_performance(): diff --git a/Tests/test_map.py b/Tests/test_map.py index 42b6f7cddab..d816bddaf3d 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,3 +1,5 @@ +import sys + import pytest from PIL import Image @@ -34,6 +36,7 @@ def test_tobytes(): Image.MAX_IMAGE_PIXELS = max_pixels +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") diff --git a/codecov.yml b/codecov.yml index 40419979fb4..1ea7974ebbe 100644 --- a/codecov.yml +++ b/codecov.yml @@ -16,6 +16,7 @@ coverage: # Matches 'omit:' in .coveragerc ignore: + - "Tests/32bit_segfault_check.py" - "Tests/bench_cffi_access.py" - "Tests/check_*.py" - "Tests/createfontdatachunk.py" diff --git a/docs/installation.rst b/docs/installation.rst index e29709bff0d..ed74dc14765 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -285,8 +285,11 @@ Many of Pillow's features require external libraries: .. tab:: Windows using MSYS2/MinGW - To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 64-bit** console, - *not* **MSYS2** directly. + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. Make sure you have Python and GCC installed:: @@ -336,6 +339,8 @@ Many of Pillow's features require external libraries: pkg install -y python ndk-sysroot clang make \ libjpeg-turbo + This has been tested within the Termux app on ChromeOS, on x86. + Installing ^^^^^^^^^^ diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index d33b75e4da3..b5edd0e361b 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -4,11 +4,6 @@ Backwards Incompatible Changes ============================== -32-bit support -^^^^^^^^^^^^^^ - -32-bit architecture is no longer supported and 32-bit wheels are no longer provided. - Categories ^^^^^^^^^^ @@ -129,14 +124,6 @@ Image.coerce_e This undocumented method has been removed. -Deprecations -============ - -TODO -^^^^ - -TODO - API Changes =========== @@ -165,6 +152,11 @@ TODO Other Changes ============= +32-bit wheels +^^^^^^^^^^^^^ + +32-bit wheels are no longer provided. + Support display_jpeg() in IPython ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/setup.py b/setup.py index b647944d3f7..7c1ad6dc5b2 100755 --- a/setup.py +++ b/setup.py @@ -153,13 +153,16 @@ def _find_library_dirs_ldconfig(): ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): - machine = os.uname()[4] + if struct.calcsize("l") == 4: + machine = os.uname()[4] + "-32" + else: + machine = os.uname()[4] + "-64" mach_map = { - "x86_64": "libc6,x86-64", - "ppc64": "libc6,64bit", - "sparc64": "libc6,64bit", - "s390x": "libc6,64bit", - "ia64": "libc6,IA-64", + "x86_64-64": "libc6,x86-64", + "ppc64-64": "libc6,64bit", + "sparc64-64": "libc6,64bit", + "s390x-64": "libc6,64bit", + "ia64-64": "libc6,IA-64", } abi_type = mach_map.get(machine, "libc6") @@ -581,7 +584,10 @@ def build_extensions(self): # user libs are at $PREFIX/lib _add_directory( library_dirs, - os.path.join(os.environ["ANDROID_ROOT"], "lib64"), + os.path.join( + os.environ["ANDROID_ROOT"], + "lib" if struct.calcsize("l") == 4 else "lib64", + ), ) elif sys.platform.startswith("netbsd"): diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index f2acabeac1a..0c0c1eda917 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -30,7 +30,7 @@ /* This is to work around a bug in GCC prior 4.9 in 64 bit mode. GCC generates code with partial dependency which is 3 times slower. See: https://stackoverflow.com/a/26588074/253146 */ -#if defined(__SSE__) && !defined(__NO_INLINE__) && \ +#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \ !defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) static float __attribute__((always_inline)) inline _i2f(int v) { float x; diff --git a/winbuild/build.rst b/winbuild/build.rst index 97df950b348..99dfad3015f 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -27,7 +27,7 @@ Download and install: * `Ninja `_ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) -* x64: `Netwide Assembler (NASM) `_ +* x86/x64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x64,ARM64}] + [--architecture {x86,x64,ARM64}] [--python PYTHON] [--executable EXECUTABLE] [--nmake] [--no-imagequant] [--no-fribidi] @@ -56,7 +56,7 @@ Run ``build_prepare.py`` to configure the build:: --depends PILLOW_DEPS directory used to store cached dependencies (default: 'winbuild\depends') - --architecture {x64,ARM64} + --architecture {x86,x64,ARM64} build architecture (default: same as host Python) --python PYTHON Python install directory (default: use host Python) --executable EXECUTABLE diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9984eb1bccc..b9de071a0a5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -3,6 +3,7 @@ import platform import re import shutil +import struct import subprocess import sys @@ -97,6 +98,7 @@ def cmd_msbuild( SF_PROJECTS = "https://sourceforge.net/projects" architectures = { + "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -609,7 +611,11 @@ def build_pillow(): choices=architectures, default=os.environ.get( "ARCHITECTURE", - "ARM64" if platform.machine() == "ARM64" else "x64", + ( + "ARM64" + if platform.machine() == "ARM64" + else ("x86" if struct.calcsize("P") == 4 else "x64") + ), ), help="build architecture (default: same as host Python)", ) From c67d73d3c927081d5364b73b3711bf1980398fb4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Jun 2023 16:36:22 +1000 Subject: [PATCH 703/727] Test 32-bit Debian 12 --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index f22733dc463..36d9c131d0b 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,6 +39,7 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-amd64, + debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-37-amd64, fedora-38-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index ed74dc14765..32676bf3f0e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -453,7 +453,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 12 Bookworm | 3.11 | x86-64 | +| Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ From 27b1acf4708dae6aaababebf993024e1744fd006 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Jun 2023 20:51:16 +1000 Subject: [PATCH 704/727] Test 32-bit Windows on AppVeyor --- .appveyor.yml | 4 ++-- docs/installation.rst | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 9a2eef76781..36f5bd0ad68 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,8 +10,8 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python311-x64 - ARCHITECTURE: x64 + - PYTHON: C:/Python311 + ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 ARCHITECTURE: x64 diff --git a/docs/installation.rst b/docs/installation.rst index 32676bf3f0e..5f3d2c9ef6a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -477,6 +477,8 @@ These platforms are built and tested for every change. | Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ +| | 3.11 | x86 | +| +----------------------------+---------------------+ | | 3.9 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.8, 3.9 (Cygwin) | x86-64 | From 1756f04acdd803a83b825cfa585527f0d78eab93 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 27 Jun 2023 21:19:32 +1000 Subject: [PATCH 705/727] Updated patch path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a9b6450d884..dd20f351c04 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -199,7 +199,7 @@ def cmd_msbuild( # link against webp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 }, - r"test/CMakeLists.txt": { + r"test\CMakeLists.txt": { "add_executable(test_write_read_tags ../placeholder.h)": "", "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501 "target_link_libraries(test_write_read_tags PRIVATE tiff)": "", From 8437d98f7fa9049f89aa501213f2e86f14dc07a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Jun 2023 11:43:05 +1000 Subject: [PATCH 706/727] Limit size even if one dimension is zero --- Tests/images/zero_width.gif | Bin 0 -> 44 bytes Tests/test_decompression_bomb.py | 9 +++++++++ src/PIL/Image.py | 2 +- src/_imagingft.c | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 Tests/images/zero_width.gif diff --git a/Tests/images/zero_width.gif b/Tests/images/zero_width.gif new file mode 100644 index 0000000000000000000000000000000000000000..da6823b60bb104bfd6969a93d71a0b8e245d836e GIT binary patch literal 44 pcmZ?wbhEHbWMEKW_{huv1pk5PKM?&_{K;|>B%lKk)MRDo2LK=z42A#z literal 0 HcmV?d00001 diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 4fd02449c7d..87681a0b557 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -64,6 +64,15 @@ def test_exception_gif_extents(self): with pytest.raises(Image.DecompressionBombError): im.seek(1) + def test_exception_gif_zero_width(self): + # Set limit to trigger exception on the test file + Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 + assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 + + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/zero_width.gif"): + pass + def test_exception_bmp(self): with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/bmp/b/reallybig.bmp"): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 97f3f492624..400edcc5b67 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3141,7 +3141,7 @@ def _decompression_bomb_check(size): if MAX_IMAGE_PIXELS is None: return - pixels = size[0] * size[1] + pixels = max(1, size[0]) * max(1, size[1]) if pixels > 2 * MAX_IMAGE_PIXELS: msg = ( diff --git a/src/_imagingft.c b/src/_imagingft.c index 6cee021d471..fd325524427 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { width += stroke_width * 2 + ceil(x_start); height += stroke_width * 2 + ceil(y_start); if (max_image_pixels != Py_None) { - if ((long long)width * height > PyLong_AsLongLong(max_image_pixels) * 2) { + if ((long long)(width > 1 ? width : 1) * (height > 1 ? height : 1) > PyLong_AsLongLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); } From 811bfe3658bc5bb5d046ba805997312353ee95e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Jun 2023 17:57:02 +1000 Subject: [PATCH 707/727] Do not use CFFI access by default on PyPy --- src/PIL/Image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 400edcc5b67..0cc82cee349 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -107,8 +107,7 @@ class DecompressionBombError(Exception): raise -# works everywhere, win for pypy, not cpython -USE_CFFI_ACCESS = hasattr(sys, "pypy_version_info") +USE_CFFI_ACCESS = False try: import cffi except ImportError: From 8a36b0fc2ddba222854f397e2066773a87b81efd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Jun 2023 20:10:10 +1000 Subject: [PATCH 708/727] Deprecate PyAccess --- Tests/test_image_access.py | 51 ++++++++++++++++++++---------------- docs/deprecations.rst | 9 +++++++ docs/releasenotes/10.0.0.rst | 10 +++++++ src/PIL/PyAccess.py | 3 +++ 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index af229d1a713..c9db3aee730 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -232,11 +232,13 @@ def test_p_putpixel_rgb_rgba(self, mode, color): assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) +@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiPutPixel(TestImagePutPixel): _need_cffi_access = True +@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiGetPixel(TestImageGetPixel): _need_cffi_access = True @@ -252,7 +254,8 @@ def _test_get_access(self, im): Using private interfaces, forcing a capi access and a pyaccess for the same image""" caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) w, h = im.size for x in range(0, w, 10): @@ -264,20 +267,16 @@ def _test_get_access(self, im): access[(access.xsize + 1, access.ysize + 1)] def test_get_vs_c(self): - rgb = hopper("RGB") - rgb.load() - self._test_get_access(rgb) - self._test_get_access(hopper("RGBA")) - self._test_get_access(hopper("L")) - self._test_get_access(hopper("LA")) - self._test_get_access(hopper("1")) - self._test_get_access(hopper("P")) - # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? - self._test_get_access(hopper("F")) + with pytest.warns(DeprecationWarning): + rgb = hopper("RGB") + rgb.load() + self._test_get_access(rgb) + for mode in ("RGBA", "L", "LA", "1", "P", "F"): + self._test_get_access(hopper(mode)) - for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): - im = Image.new(mode, (10, 10), 40000) - self._test_get_access(im) + for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): + im = Image.new(mode, (10, 10), 40000) + self._test_get_access(im) # These don't actually appear to be modes that I can actually make, # as unpack sets them directly into the I mode. @@ -292,7 +291,8 @@ def _test_set_access(self, im, color): Using private interfaces, forcing a capi access and a pyaccess for the same image""" caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) w, h = im.size for x in range(0, w, 10): @@ -301,13 +301,15 @@ def _test_set_access(self, im, color): assert color == caccess[(x, y)] # Attempt to set the value on a read-only image - access = PyAccess.new(im, True) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, True) with pytest.raises(ValueError): access[(0, 0)] = color def test_set_vs_c(self): rgb = hopper("RGB") - rgb.load() + with pytest.warns(DeprecationWarning): + rgb.load() self._test_set_access(rgb, (255, 128, 0)) self._test_set_access(hopper("RGBA"), (255, 192, 128, 0)) self._test_set_access(hopper("L"), 128) @@ -326,6 +328,7 @@ def test_set_vs_c(self): # im = Image.new('I;32B', (10, 10), 2**10) # self._test_set_access(im, 2**13-1) + @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_not_implemented(self): assert PyAccess.new(hopper("BGR;15")) is None @@ -335,7 +338,8 @@ def test_reference_counting(self): for _ in range(10): # Do not save references to the image, only to the access object - px = Image.new("L", (size, 1), 0).load() + with pytest.warns(DeprecationWarning): + px = Image.new("L", (size, 1), 0).load() for i in range(size): # pixels can contain garbage if image is released assert px[i, 0] == 0 @@ -344,12 +348,13 @@ def test_reference_counting(self): def test_p_putpixel_rgb_rgba(self, mode): for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) - access = PyAccess.new(im, False) - access.putpixel((0, 0), color) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) + access.putpixel((0, 0), color) - if len(color) == 3: - color += (255,) - assert im.convert("RGBA").getpixel((0, 0)) == color + if len(color) == 3: + color += (255,) + assert im.convert("RGBA").getpixel((0, 0)) == color class TestImagePutPixelError(AccessTest): diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 62687d869e8..b68663d358b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -22,6 +22,15 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. +PyAccess +~~~~~~~~ + +.. deprecated:: 10.0.0 + +Since Pillow's C API is now faster than PyAccess on PyPy, +:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +11.0.0 (2024-10-15). + Removed features ---------------- diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index b5edd0e361b..afb4164e964 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -124,6 +124,16 @@ Image.coerce_e This undocumented method has been removed. +Deprecations +============ + +PyAccess +^^^^^^^^ + +Since Pillow's C API is now faster than PyAccess on PyPy, +:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +11.0.0 (2024-10-15). + API Changes =========== diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 39747b4f311..99b46a4a66c 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -22,6 +22,8 @@ import logging import sys +from ._deprecate import deprecate + try: from cffi import FFI @@ -47,6 +49,7 @@ class PyAccess: def __init__(self, img, readonly=False): + deprecate("PyAccess", 11) vals = dict(img.im.unsafe_ptrs) self.readonly = readonly self.image8 = ffi.cast("unsigned char **", vals["image8"]) From 1a185973dd173e07ec9d2c125ff1980844d996f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Jun 2023 22:00:32 +1000 Subject: [PATCH 709/727] Mention default behaviour change --- docs/deprecations.rst | 2 +- docs/releasenotes/10.0.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index b68663d358b..c79b8966322 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -29,7 +29,7 @@ PyAccess Since Pillow's C API is now faster than PyAccess on PyPy, :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow -11.0.0 (2024-10-15). +11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. Removed features ---------------- diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index afb4164e964..94761efafb6 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -132,7 +132,7 @@ PyAccess Since Pillow's C API is now faster than PyAccess on PyPy, :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow -11.0.0 (2024-10-15). +11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. API Changes =========== From b5ce3193b68569baee207768bade77b0fb1643e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 08:30:10 +1000 Subject: [PATCH 710/727] Deprecate Image.USE_CFFI_ACCESS --- docs/deprecations.rst | 7 +++++-- docs/releasenotes/10.0.0.rst | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c79b8966322..ce956cadeff 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -22,8 +22,8 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. -PyAccess -~~~~~~~~ +PyAccess and Image.USE_CFFI_ACCESS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 10.0.0 @@ -31,6 +31,9 @@ Since Pillow's C API is now faster than PyAccess on PyPy, :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow 11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is +similarly deprecated. + Removed features ---------------- diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 94761efafb6..9b92e27d84f 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -127,13 +127,16 @@ This undocumented method has been removed. Deprecations ============ -PyAccess -^^^^^^^^ +PyAccess and Image.USE_CFFI_ACCESS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Since Pillow's C API is now faster than PyAccess on PyPy, :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow 11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is +similarly deprecated. + API Changes =========== From 456ef61cb58219daafd68ed82fedf849e8ca0d80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 09:43:06 +1000 Subject: [PATCH 711/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9d95f3cd257..af695bcb045 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,8 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- -- Removed support for 32-bit #7228 - [radarhere, hugovk] +- Limit size even if one dimension is zero in decompression bomb check #7235 + [radarhere] - Use --config-settings instead of deprecated --global-option #7171 [radarhere] From 444b8118bdccb021ca37df4c173789b31e77e8ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 15:11:16 +1000 Subject: [PATCH 712/727] Updated libwebp to 1.3.1 --- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/depends/install_webp.sh b/depends/install_webp.sh index f8b985a7a02..4636aab437b 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.3.0 +archive=libwebp-1.3.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b9de071a0a5..f1f20422b6c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -165,9 +165,9 @@ def cmd_msbuild( "libs": [r"liblzma.lib"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz", - "filename": "libwebp-1.3.0.tar.gz", - "dir": "libwebp-1.3.0", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.1.tar.gz", + "filename": "libwebp-1.3.1.tar.gz", + "dir": "libwebp-1.3.1", "license": "COPYING", "build": [ cmd_rmdir(r"output\release-static"), # clean From ae43cda4c5c5c3935624a98258571040a71e026b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 20:58:43 +1000 Subject: [PATCH 713/727] Added release notes for #7235 --- docs/releasenotes/10.0.0.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 9b92e27d84f..01b15f38696 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -157,10 +157,15 @@ TODO Security ======== -TODO -^^^^ +Limit size even if one dimension is zero +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +When performing decompression bomb checks, Pillow did not reject images with +excessive width and zero height, or zero width and excessive height. That has +now been fixed. + +This effectively dates to the PIL fork, since problem images would still have +been processed before Pillow started checking for decompression bombs. Other Changes ============= From 49cde0ad3d1234cf14db2d2159ab3b95e7e929d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 21:03:42 +1000 Subject: [PATCH 714/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index af695bcb045..c6b6305d8f1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Do not use CFFI access by default on PyPy #7236 + [radarhere] + - Limit size even if one dimension is zero in decompression bomb check #7235 [radarhere] From 07404991512d370723ef835b0954d2fcb4eee883 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Jun 2023 09:05:59 +1000 Subject: [PATCH 715/727] Prioritise speed in _repr_png_ --- src/PIL/Image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0464513fdb0..08bb5615b4e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -632,7 +632,7 @@ def _repr_pretty_(self, p, cycle): ) ) - def _repr_image(self, image_format): + def _repr_image(self, image_format, **kwargs): """Helper function for iPython display hook. :param image_format: Image format. @@ -640,7 +640,7 @@ def _repr_image(self, image_format): """ b = io.BytesIO() try: - self.save(b, image_format) + self.save(b, image_format, **kwargs) except Exception as e: msg = f"Could not save to {image_format} for display" raise ValueError(msg) from e @@ -651,7 +651,7 @@ def _repr_png_(self): :returns: PNG version of the image as bytes """ - return self._repr_image("PNG") + return self._repr_image("PNG", compress_level=1) def _repr_jpeg_(self): """iPython display hook support for JPEG format. From 0fb69fa821155c1b213f3f3488d3057b6ba7c154 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Jun 2023 16:59:36 +1000 Subject: [PATCH 716/727] Added release notes for #7123 --- docs/releasenotes/10.0.0.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 01b15f38696..94ff04d46a2 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -149,10 +149,13 @@ An optional line ``width`` parameter has been added to API Additions ============= -TODO -^^^^ +Added ``alpha_only`` argument to ``getbbox()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:py:meth:`~PIL.Image.Image.getbbox` now accepts a keyword argument of +``alpha_only``. This is an optional flag, defaulting to ``True``. If ``True`` +and the image has an alpha channel, trim transparent pixels. Otherwise, trim +pixels when all channels are zero. Security ======== From 1fe1bb49c452b0318cad12ea9d97c3bef188e9a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Jun 2023 23:32:26 +1000 Subject: [PATCH 717/727] Added ImageFont.MAX_STRING_LENGTH --- Tests/test_imagefont.py | 19 +++++++++++++++++++ docs/reference/ImageFont.rst | 18 ++++++++++++++++++ docs/releasenotes/10.0.0.rst | 12 ++++++++++++ src/PIL/ImageFont.py | 15 +++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 7fa8ff8cbfd..c50447a153d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1038,6 +1038,25 @@ def test_render_mono_size(): assert_image_equal_tofile(im, "Tests/images/text_mono.gif") +def test_too_many_characters(font): + with pytest.raises(ValueError): + font.getlength("A" * 1000001) + with pytest.raises(ValueError): + font.getbbox("A" * 1000001) + with pytest.raises(ValueError): + font.getmask2("A" * 1000001) + + transposed_font = ImageFont.TransposedFont(font) + with pytest.raises(ValueError): + transposed_font.getlength("A" * 1000001) + + default_font = ImageFont.load_default() + with pytest.raises(ValueError): + default_font.getlength("A" * 1000001) + with pytest.raises(ValueError): + default_font.getbbox("A" * 1000001) + + @pytest.mark.parametrize( "test_file", [ diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 946bd3c4bed..2abfa0cc997 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -18,6 +18,15 @@ OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of the imToolkit package. +.. warning:: + To protect against potential DOS attacks when using arbitrary strings as + text input, Pillow will raise a ``ValueError`` if the number of characters + is over a certain limit, :py:data:`MAX_STRING_LENGTH`. + + This threshold can be changed by setting + :py:data:`MAX_STRING_LENGTH`. It can be disabled by setting + ``ImageFont.MAX_STRING_LENGTH = None``. + Example ------- @@ -73,3 +82,12 @@ Constants Requires Raqm, you can check support using :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + +Constants +--------- + +.. data:: MAX_STRING_LENGTH + + Set to 1,000,000, to protect against potential DOS attacks. Pillow will + raise a ``ValueError`` if the number of characters is over this limit. The + check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 94ff04d46a2..4cd6293229a 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -170,6 +170,18 @@ now been fixed. This effectively dates to the PIL fork, since problem images would still have been processed before Pillow started checking for decompression bombs. +Added ImageFont.MAX_STRING_LENGTH +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using arbitrary strings as text +input, Pillow will now raise a ``ValueError`` if the number of characters +passed into ImageFont methods is over a certain limit, +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. + +This threshold can be changed by setting +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting +``ImageFont.MAX_STRING_LENGTH = None``. + Other Changes ============= diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 3ddc1aaad64..1030985ebc4 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -41,6 +41,9 @@ class Layout(IntEnum): RAQM = 1 +MAX_STRING_LENGTH = 1000000 + + try: from . import _imagingft as core except ImportError as ex: @@ -49,6 +52,12 @@ class Layout(IntEnum): core = DeferredError(ex) +def _string_length_check(text): + if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: + msg = "too many characters in string" + raise ValueError(msg) + + # FIXME: add support for pilfont2 format (see FontFile.py) # -------------------------------------------------------------------- @@ -152,6 +161,7 @@ def getbbox(self, text, *args, **kwargs): :return: ``(left, top, right, bottom)`` bounding box """ + _string_length_check(text) width, height = self.font.getsize(text) return 0, 0, width, height @@ -162,6 +172,7 @@ def getlength(self, text, *args, **kwargs): .. versionadded:: 9.2.0 """ + _string_length_check(text) width, height = self.font.getsize(text) return width @@ -309,6 +320,7 @@ def getlength(self, text, mode="", direction=None, features=None, language=None) :return: Width for horizontal, height for vertical text. """ + _string_length_check(text) return self.font.getlength(text, mode, direction, features, language) / 64 def getbbox( @@ -368,6 +380,7 @@ def getbbox( :return: ``(left, top, right, bottom)`` bounding box """ + _string_length_check(text) size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) @@ -546,6 +559,7 @@ def getmask2( :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ + _string_length_check(text) if start is None: start = (0, 0) im, size, offset = self.font.render( @@ -684,6 +698,7 @@ def getlength(self, text, *args, **kwargs): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) + _string_length_check(text) return self.font.getlength(text, *args, **kwargs) From d398fedb9d5af22316c715d2066176d15031d439 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 1 Jul 2023 07:25:18 +1000 Subject: [PATCH 718/727] Added underscores for readability Co-authored-by: Hugo van Kemenade --- Tests/test_imagefont.py | 12 ++++++------ src/PIL/ImageFont.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index c50447a153d..02622e72138 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1040,21 +1040,21 @@ def test_render_mono_size(): def test_too_many_characters(font): with pytest.raises(ValueError): - font.getlength("A" * 1000001) + font.getlength("A" * 1_000_001) with pytest.raises(ValueError): - font.getbbox("A" * 1000001) + font.getbbox("A" * 1_000_001) with pytest.raises(ValueError): - font.getmask2("A" * 1000001) + font.getmask2("A" * 1_000_001) transposed_font = ImageFont.TransposedFont(font) with pytest.raises(ValueError): - transposed_font.getlength("A" * 1000001) + transposed_font.getlength("A" * 1_000_001) default_font = ImageFont.load_default() with pytest.raises(ValueError): - default_font.getlength("A" * 1000001) + default_font.getlength("A" * 1_000_001) with pytest.raises(ValueError): - default_font.getbbox("A" * 1000001) + default_font.getbbox("A" * 1_000_001) @pytest.mark.parametrize( diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 1030985ebc4..b7d40208c83 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -41,7 +41,7 @@ class Layout(IntEnum): RAQM = 1 -MAX_STRING_LENGTH = 1000000 +MAX_STRING_LENGTH = 1_000_000 try: From 8c1dc819fd91471825da01976ac0e0bc8789590f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 1 Jul 2023 10:31:34 +1000 Subject: [PATCH 719/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c6b6305d8f1..8ec5d33ed0e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,24 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added ImageFont.MAX_STRING_LENGTH #7244 + [radarhere, hugovk] + +- Fix Windows build with pyproject.toml #7230 + [hugovk, nulano, radarhere] + +- Do not close provided file handles with libtiff #7199 + [radarhere] + +- Convert to HSV if mode is HSV in getcolor() #7226 + [radarhere] + +- Added alpha_only argument to getbbox() #7123 + [radarhere. hugovk] + +- Prioritise speed in _repr_png_ #7242 + [radarhere] + - Do not use CFFI access by default on PyPy #7236 [radarhere] From 39a3b1d83edcf826c3864e26bedff5b4e4dd331b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Jul 2023 18:09:27 +1000 Subject: [PATCH 720/727] Fixed deallocating mask images --- src/PIL/ImageFont.py | 12 ++++++++++-- src/_imagingft.c | 7 ++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b7d40208c83..05828a72fdf 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -562,9 +562,17 @@ def getmask2( _string_length_check(text) if start is None: start = (0, 0) - im, size, offset = self.font.render( + im = None + + def fill(mode, size): + nonlocal im + + im = Image.core.fill(mode, size) + return im + + size, offset = self.font.render( text, - Image.core.fill, + fill, mode, direction, features, diff --git a/src/_imagingft.c b/src/_imagingft.c index fd325524427..62819a569bc 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -882,7 +882,7 @@ font_render(FontObject *self, PyObject *args) { if (max_image_pixels != Py_None) { if ((long long)(width > 1 ? width : 1) * (height > 1 ? height : 1) > PyLong_AsLongLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); - return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); + return Py_BuildValue("(ii)(ii)", width, height, 0, 0); } } @@ -898,7 +898,7 @@ font_render(FontObject *self, PyObject *args) { y_offset -= stroke_width; if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); - return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); + return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); } if (stroke_width) { @@ -1113,9 +1113,10 @@ font_render(FontObject *self, PyObject *args) { if (bitmap_converted_ready) { FT_Bitmap_Done(library, &bitmap_converted); } + Py_DECREF(image); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); + return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); glyph_error: if (im->destroy) { From 6e28ed1f36d0eb74053af54e1eddc9c29db698cd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Jul 2023 22:08:11 +1000 Subject: [PATCH 721/727] 10.0.0 version bump --- CHANGES.rst | 5 ++++- src/PIL/_version.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8ec5d33ed0e..94cd6e7bc75 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,12 @@ Changelog (Pillow) ================== -10.0.0 (unreleased) +10.0.0 (2023-07-01) ------------------- +- Fixed deallocating mask images #7246 + [radarhere] + - Added ImageFont.MAX_STRING_LENGTH #7244 [radarhere, hugovk] diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 800203d5170..1fc7f7334aa 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "10.0.0.dev0" +__version__ = "10.0.0" From b0e28048d692effadfe7a4268a03e1d20e0198bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Aug 2023 22:22:51 +1000 Subject: [PATCH 722/727] Updated zlib to 1.3 --- Tests/test_file_png.py | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c4db9790524..b460761d838 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -79,7 +79,7 @@ def get_chunks(self, filename): def test_sanity(self, tmp_path): # internal version number - assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) + assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) test_file = str(tmp_path / "temp.png") diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5a5bb8e0ade..960d2886cf0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -130,9 +130,9 @@ def cmd_msbuild( "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/zlib1213.zip", - "filename": "zlib1213.zip", - "dir": "zlib-1.2.13", + "url": "https://zlib.net/zlib13.zip", + "filename": "zlib13.zip", + "dir": "zlib-1.3", "license": "README", "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ From 730f74600e8215ab510f71bb1fbb49d906c4356b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 Sep 2023 15:32:33 +1000 Subject: [PATCH 723/727] Updated libwebp to 1.3.2 --- 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 4636aab437b..6f867ab3788 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.3.1 +archive=libwebp-1.3.2 ./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 960d2886cf0..a88ec7a095a 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -157,9 +157,9 @@ def cmd_msbuild( "libs": [r"liblzma.lib"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.1.tar.gz", - "filename": "libwebp-1.3.1.tar.gz", - "dir": "libwebp-1.3.1", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.2.tar.gz", + "filename": "libwebp-1.3.2.tar.gz", + "dir": "libwebp-1.3.2", "license": "COPYING", "build": [ cmd_rmdir(r"output\release-static"), # clean From b4c7d4b8b2710b7af6cc944a804902eb75fd9056 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Sep 2023 21:22:29 +1000 Subject: [PATCH 724/727] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 94cd6e7bc75..e1d052e02e2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,15 @@ Changelog (Pillow) ================== +10.0.1 (2023-09-15) +------------------- + +- Updated zlib to 1.3 #7344 + [radarhere] + +- Updated libwebp to 1.3.2 #7395 + [radarhere] + 10.0.0 (2023-07-01) ------------------- From d50250d9eab741ae3ddd592d8910cfd7973b9d35 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Sep 2023 21:37:50 +1000 Subject: [PATCH 725/727] Added release notes for 10.0.1 --- docs/releasenotes/10.0.1.rst | 14 ++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 15 insertions(+) create mode 100644 docs/releasenotes/10.0.1.rst diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst new file mode 100644 index 00000000000..df4ae5dd9fd --- /dev/null +++ b/docs/releasenotes/10.0.1.rst @@ -0,0 +1,14 @@ +10.0.1 +------ + +Updated tests to pass with latest zlib version +============================================== + +The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail. + +Security +======== + +This release addresses :cve:`2023-4863`, by providing an updated install script and +updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow +in WebP. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 9bca9854152..1dee0715372 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.0.1 10.0.0 9.5.0 9.4.0 From a62f2402a6bcf11a0a1670542216725a3f9190e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Sep 2023 21:31:05 +1000 Subject: [PATCH 726/727] 10.0.1 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 1fc7f7334aa..f3455f1f1f7 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "10.0.0" +__version__ = "10.0.1" From e34d346f10c0b1c814661e662a3e0c1ef084cf1c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Sep 2023 21:55:25 +1000 Subject: [PATCH 727/727] Updated order --- CHANGES.rst | 4 ++-- docs/releasenotes/10.0.1.rst | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e1d052e02e2..b4dc1d6646e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,10 +5,10 @@ Changelog (Pillow) 10.0.1 (2023-09-15) ------------------- -- Updated zlib to 1.3 #7344 +- Updated libwebp to 1.3.2 #7395 [radarhere] -- Updated libwebp to 1.3.2 #7395 +- Updated zlib to 1.3 #7344 [radarhere] 10.0.0 (2023-07-01) diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst index df4ae5dd9fd..6ac30e7fce1 100644 --- a/docs/releasenotes/10.0.1.rst +++ b/docs/releasenotes/10.0.1.rst @@ -1,14 +1,14 @@ 10.0.1 ------ -Updated tests to pass with latest zlib version -============================================== - -The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail. - Security ======== This release addresses :cve:`2023-4863`, by providing an updated install script and updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow in WebP. + +Updated tests to pass with latest zlib version +============================================== + +The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail.