From e10a58ccce18c3493e30dfe7222076964311a8d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Oct 2023 21:46:38 +1100 Subject: [PATCH 001/131] Concatenate multiple EXIF markers --- Tests/images/multiple_exif.jpg | Bin 0 -> 364 bytes Tests/test_file_jpeg.py | 4 ++++ src/PIL/JpegImagePlugin.py | 10 ++++++---- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 Tests/images/multiple_exif.jpg diff --git a/Tests/images/multiple_exif.jpg b/Tests/images/multiple_exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32e0aa301a94dfa13dc47faeb3071c25a5d8b573 GIT binary patch literal 364 zcmex=v-)3-T z;9z58XJh4HXJ_Z+gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI v_1g6tH*Y Date: Fri, 24 Nov 2023 15:19:19 +1100 Subject: [PATCH 002/131] Do not assign new fp attribute to image when exiting context manager --- Tests/test_image.py | 5 +++++ src/PIL/Image.py | 17 +++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index f0861bb4f81..98a82cfaa61 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1015,6 +1015,11 @@ def test_fli_overrun2(self): except OSError as e: assert str(e) == "buffer overrun when reading image file" + def test_exit_fp(self): + with Image.new("L", (1, 1)) as im: + pass + assert not hasattr(im, "fp") + def test_close_graceful(self, caplog): with Image.open("Tests/images/hopper.jpg") as im: copy = im.copy() diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d435b561720..b6ac1f0825b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -528,14 +528,15 @@ def __enter__(self): return self def __exit__(self, *args): - if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False): - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() - self.fp = None + if hasattr(self, "fp"): + if getattr(self, "_exclusive_fp", False): + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + self.fp = None def close(self): """ From cd9deddcd5e09c4a31621a21acb2f059296162b1 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 28 Nov 2023 00:50:42 +0100 Subject: [PATCH 003/131] add gcc problem matcher to test.yml --- .github/problem-matchers/gcc.json | 18 ++++++++++++++++++ .github/workflows/test.yml | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 .github/problem-matchers/gcc.json diff --git a/.github/problem-matchers/gcc.json b/.github/problem-matchers/gcc.json new file mode 100644 index 00000000000..8e2866afe23 --- /dev/null +++ b/.github/problem-matchers/gcc.json @@ -0,0 +1,18 @@ +{ + "__comment": "Based on vscode-cpptools' Extension/package.json gcc rule", + "problemMatcher": [ + { + "owner": "gcc-problem-matcher", + "pattern": [ + { + "regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33dc561e561..9e7d4ed628f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,6 +86,10 @@ jobs: env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + - name: Register gcc problem matcher + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" + run: echo "::add-matcher::.github/problem-matchers/gcc.json" + - name: Build run: | .ci/build.sh From 5fb86c55ed52fecf61daa9c9e93108abc1831948 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Nov 2023 20:05:17 +1100 Subject: [PATCH 004/131] Moved code closing fp and _fp into common method --- src/PIL/Image.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b6ac1f0825b..7e0e14f2636 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -527,15 +527,18 @@ def _new(self, im): def __enter__(self): return self + def _close_fp(self): + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + def __exit__(self, *args): if hasattr(self, "fp"): if getattr(self, "_exclusive_fp", False): - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() + self._close_fp() self.fp = None def close(self): @@ -552,12 +555,7 @@ def close(self): """ if hasattr(self, "fp"): try: - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() + self._close_fp() self.fp = None except Exception as msg: logger.debug("Error closing: %s", msg) From f1fef09d4ae34a060296ef312e908a3f70413d2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Nov 2023 21:05:53 +1100 Subject: [PATCH 005/131] Support arbitrary masks for uncompressed RGB images --- Tests/images/bgr15.dds | Bin 0 -> 32896 bytes Tests/images/bgr15.png | Bin 0 -> 17923 bytes Tests/test_file_dds.py | 2 ++ src/PIL/DdsImagePlugin.py | 48 +++++++++++++++++++++++++++++++++----- 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 Tests/images/bgr15.dds create mode 100644 Tests/images/bgr15.png diff --git a/Tests/images/bgr15.dds b/Tests/images/bgr15.dds new file mode 100644 index 0000000000000000000000000000000000000000..ba3bbddcae4c59aeb3049fc46527a94b1fadda55 GIT binary patch literal 32896 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 (offset + 1) << (offset + 1) == mask: + offset += 1 + mask_offsets.append(offset) + mask_totals.append(mask >> offset) + + data = bytearray() + bytecount = bitcount // 8 + while len(data) < self.state.xsize * self.state.ysize * len(masks): + value = self.fd.read(bytecount) + int_value = sum(value[i] << i * 8 for i in range(bytecount)) + for i, mask in enumerate(masks): + masked_value = int_value & mask + # Remove the zero padding, and scale it to 8 bits + data += o8( + int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) + ) + self.set_as_raw(bytes(data)) + return -1, 0 + + def _save(im, fp, filename): if im.mode not in ("RGB", "RGBA", "L", "LA"): msg = f"cannot write mode {im.mode} as DDS" @@ -291,5 +326,6 @@ def _accept(prefix): Image.register_open(DdsImageFile.format, DdsImageFile, _accept) +Image.register_decoder("dds_rgb", DdsRgbDecoder) Image.register_save(DdsImageFile.format, _save) Image.register_extension(DdsImageFile.format, ".dds") From 30eb41475dacea619c4660555788ec73d32df2a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Dec 2023 22:44:51 +1100 Subject: [PATCH 006/131] Use f-string Co-authored-by: Aarni Koskela --- src/PIL/DdsImagePlugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 65d9deaa0d4..731c85a7e72 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -154,9 +154,7 @@ def _open(self): self._mode = "RGB" mask_count = 3 - masks = struct.unpack( - "<" + str(mask_count) + "I", header.read(mask_count * 4) - ) + masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4)) self.tile = [("dds_rgb", (0, 0) + self.size, 0, (bitcount, masks))] elif pfflags & DDPF_PALETTEINDEXED8: self._mode = "P" From 2e8dd3bdcaf3fa13884ee3ca035c32ba9c4f7fcd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Dec 2023 22:56:25 +1100 Subject: [PATCH 007/131] Use int.from_bytes() --- src/PIL/DdsImagePlugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 731c85a7e72..17b1a60824e 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -258,10 +258,9 @@ def decode(self, buffer): data = bytearray() bytecount = bitcount // 8 while len(data) < self.state.xsize * self.state.ysize * len(masks): - value = self.fd.read(bytecount) - int_value = sum(value[i] << i * 8 for i in range(bytecount)) + value = int.from_bytes(self.fd.read(bytecount), "little") for i, mask in enumerate(masks): - masked_value = int_value & mask + masked_value = value & mask # Remove the zero padding, and scale it to 8 bits data += o8( int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) From 232094e065e8667f3d8d6c2132c44d9426c86ef6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 21:45:19 +0000 Subject: [PATCH 008/131] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/DdsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 2309cb4063d..5a162768676 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -16,8 +16,8 @@ from enum import IntEnum, IntFlag from . import Image, ImageFile, ImagePalette -from ._binary import o8 from ._binary import i32le as i32 +from ._binary import o8 from ._binary import o32le as o32 # Magic ("DDS ") From 4b422db2439d1858cc71f38e706775daa419b692 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 24 Oct 2023 23:55:19 -0500 Subject: [PATCH 009/131] Add keep_rgb option to prevent RGB -> YCbCr conversion during JPEG write libjpeg automatically converts RGB to YCbCr by default. Add a keep_rgb option to disable libjpeg's automatic conversion of RGB images during write. --- Tests/test_file_jpeg.py | 14 ++++++++++++++ docs/handbook/image-file-formats.rst | 7 +++++++ src/PIL/JpegImagePlugin.py | 1 + src/encode.c | 5 ++++- src/libImaging/Jpeg.h | 3 +++ src/libImaging/JpegEncode.c | 14 ++++++++++++++ 6 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ef070b6c5ba..3b11c1123ca 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -141,6 +141,16 @@ def test_cmyk(self): ) assert k > 0.9 + def test_rgb(self): + def getchannels(im): + return tuple(v[0] for v in im.layer) + + im = self.roundtrip(hopper()) + assert getchannels(im) == (1, 2, 3) + im = self.roundtrip(hopper(), keep_rgb=True) + assert getchannels(im) == (ord("R"), ord("G"), ord("B")) + assert_image_similar(hopper(), im, 12) + @pytest.mark.parametrize( "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], @@ -445,6 +455,10 @@ def getsampling(im): with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") + # RGB colorspace, no subsampling by default + im = self.roundtrip(hopper(), subsampling=3, keep_rgb=True) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + def test_exif(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 23da312a600..46998e03a11 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -486,6 +486,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exif** If present, the image will be stored with the provided raw EXIF data. +**keep_rgb** + By default, libjpeg converts images with an RGB color space to YCbCr. + If this option is present and true, those images will be stored as RGB + instead. + + .. versionadded:: 10.2.0 + **subsampling** If present, sets the subsampling for the encoder. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 5add65f4542..00548e8b7a4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -781,6 +781,7 @@ def validate_qtables(qtables): progressive, info.get("smooth", 0), optimize, + info.get("keep_rgb", False), info.get("streamtype", 0), dpi[0], dpi[1], diff --git a/src/encode.c b/src/encode.c index 4664ad0f32a..c7dd510150e 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { Py_ssize_t progressive = 0; Py_ssize_t smooth = 0; Py_ssize_t optimize = 0; + int keep_rgb = 0; Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ Py_ssize_t xdpi = 0, ydpi = 0; Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ @@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnnnOz#y#y#", + "ss|nnnnpnnnnnnOz#y#y#", &mode, &rawmode, &quality, &progressive, &smooth, &optimize, + &keep_rgb, &streamtype, &xdpi, &ydpi, @@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); + ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb; ((JPEGENCODERSTATE *)encoder->state.context)->quality = quality; ((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays; ((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 5cc74e69bf5..98eaac28dd6 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -74,6 +74,9 @@ typedef struct { /* Optimize Huffman tables (slow) */ int optimize; + /* Disable automatic conversion of RGB images to YCbCr if nonzero */ + int keep_rgb; + /* Stream type (0=full, 1=tables only, 2=image only) */ int streamtype; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 9da830b186f..2946cc530a1 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -137,6 +137,20 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Compressor configuration */ jpeg_set_defaults(&context->cinfo); + /* Prevent RGB -> YCbCr conversion */ + if (context->keep_rgb) { + switch (context->cinfo.in_color_space) { + case JCS_RGB: +#ifdef JCS_EXTENSIONS + case JCS_EXT_RGBX: +#endif + jpeg_set_colorspace(&context->cinfo, JCS_RGB); + break; + default: + break; + } + } + /* Use custom quantization tables */ if (context->qtables) { int i; From f90827dfc881c1dbbebe27aed78abc279a09f0be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Nov 2023 14:35:23 +1100 Subject: [PATCH 010/131] Rearranged subsampling assertions --- Tests/test_file_jpeg.py | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3b11c1123ca..e270820bdcc 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -145,11 +145,13 @@ def test_rgb(self): def getchannels(im): return tuple(v[0] for v in im.layer) - im = self.roundtrip(hopper()) - assert getchannels(im) == (1, 2, 3) - im = self.roundtrip(hopper(), keep_rgb=True) - assert getchannels(im) == (ord("R"), ord("G"), ord("B")) - assert_image_similar(hopper(), im, 12) + im = hopper() + im_ycbcr = self.roundtrip(im) + assert getchannels(im_ycbcr) == (1, 2, 3) + + im_rgb = self.roundtrip(im, keep_rgb=True) + assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B")) + assert_image_similar(im, im_rgb, 12) @pytest.mark.parametrize( "test_image_path", @@ -434,31 +436,26 @@ def getsampling(im): # experimental API im = self.roundtrip(hopper(), subsampling=-1) # default assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=0) # 4:4:4 - assert getsampling(im) == (1, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=1) # 4:2:2 - assert getsampling(im) == (2, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=2) # 4:2:0 - assert getsampling(im) == (2, 2, 1, 1, 1, 1) + for subsampling in (0, "4:4:4"): + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + for subsampling in (1, "4:2:2"): + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (2, 1, 1, 1, 1, 1) + for subsampling in (2, "4:2:0", "4:1:1"): + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) + im = self.roundtrip(hopper(), subsampling=3) # default (undefined) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:4:4") + # RGB colorspace, no subsampling by default + im = self.roundtrip(hopper(), subsampling=3, keep_rgb=True) assert getsampling(im) == (1, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:2:2") - assert getsampling(im) == (2, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:2:0") - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:1:1") - assert getsampling(im) == (2, 2, 1, 1, 1, 1) with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") - # RGB colorspace, no subsampling by default - im = self.roundtrip(hopper(), subsampling=3, keep_rgb=True) - assert getsampling(im) == (1, 1, 1, 1, 1, 1) - def test_exif(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() From 14146732be77093e57622cbc367311219cc64c29 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Mon, 4 Dec 2023 06:12:29 -0600 Subject: [PATCH 011/131] Clarify JPEG tests for default/invalid subsampling -1 is the default; 3 is invalid and should behave the same as the default. --- Tests/test_file_jpeg.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index e270820bdcc..748c07119ba 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -434,8 +434,9 @@ def getsampling(im): return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] # experimental API - im = self.roundtrip(hopper(), subsampling=-1) # default - assert getsampling(im) == (2, 2, 1, 1, 1, 1) + for subsampling in (-1, 3): # (default, invalid) + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) for subsampling in (0, "4:4:4"): im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (1, 1, 1, 1, 1, 1) @@ -446,11 +447,8 @@ def getsampling(im): im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=3) # default (undefined) - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - # RGB colorspace, no subsampling by default - im = self.roundtrip(hopper(), subsampling=3, keep_rgb=True) + im = self.roundtrip(hopper(), keep_rgb=True) assert getsampling(im) == (1, 1, 1, 1, 1, 1) with pytest.raises(TypeError): From a5fab5fc0b559136120d9d6bea73896dacd0e3d7 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Mon, 4 Dec 2023 07:31:24 -0600 Subject: [PATCH 012/131] Fail if chroma subsampling selected when writing RGB JPEG The user presumably doesn't intend to subsample the green and blue channels. --- Tests/test_file_jpeg.py | 12 +++++++++--- docs/handbook/image-file-formats.rst | 3 +++ src/libImaging/JpegEncode.c | 10 ++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 748c07119ba..75851e4773e 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -447,9 +447,15 @@ def getsampling(im): im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - # RGB colorspace, no subsampling by default - im = self.roundtrip(hopper(), keep_rgb=True) - assert getsampling(im) == (1, 1, 1, 1, 1, 1) + # RGB colorspace + for subsampling in (-1, 0, "4:4:4"): + # "4:4:4" doesn't really make sense for RGB, but the conversion + # to an integer happens at a higher level + im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + for subsampling in (1, "4:2:2", 2, "4:2:0", 3): + with pytest.raises(OSError): + self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 46998e03a11..773d517efc6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -491,6 +491,9 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If this option is present and true, those images will be stored as RGB instead. + When this option is enabled, attempting to chroma-subsample RGB images + with the ``subsampling`` option will raise an :py:exc:`OSError`. + .. versionadded:: 10.2.0 **subsampling** diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 2946cc530a1..00f3d5f74db 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -144,6 +144,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { #ifdef JCS_EXTENSIONS case JCS_EXT_RGBX: #endif + switch (context->subsampling) { + case -1: /* Default */ + case 0: /* No subsampling */ + break; + default: + /* Would subsample the green and blue + channels, which doesn't make sense */ + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } jpeg_set_colorspace(&context->cinfo, JCS_RGB); break; default: From e2018a6697d5e1cbee4eb29da887f8fc9b0b60f4 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Wed, 13 Dec 2023 18:27:55 -0600 Subject: [PATCH 013/131] Add release note for JPEG keep_rgb option Co-authored-by: Andrew Murray --- docs/releasenotes/10.2.0.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 9883f10baf3..284de7f4f9c 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -46,6 +46,14 @@ Added DdsImagePlugin enums :py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT` enums have been added to :py:class:`PIL.DdsImagePlugin`. +JPEG RGB color space +^^^^^^^^^^^^^^^^^^^^ + +When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB +images in the RGB color space instead of being converted to YCbCr automatically by +libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with +the ``subsampling`` option will raise an :py:exc:`OSError`. + JPEG restart marker interval ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From a72b5963d73c90017bd3ab457df52256d8812eff Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Mon, 11 Dec 2023 04:14:18 -0600 Subject: [PATCH 014/131] Document JPEG 2000 support for writing YCbCr and reading subsampled color Read support for subsampled RGB and YCbCr color channels was added in 4f4c3b34f8 and not documented at the time. Write support for YCbCr appears to date to 61fb89ec54, the original commit. Retain the existing language about YCbCr input conversion to RGB, even though it's not completely correct. OpenJPEG through 2.5.0 doesn't set color_space in opj_read_header(), so we end up in our OPJ_CLRSPC_UNSPECIFIED fallback path, which guesses sRGB if there's no component subsampling. This means we currently can't round-trip YCbCr via JPEG 2000. The next OpenJPEG release will fix this, so leave the docs as is. Also fix typo: .j2p -> .jp2. --- docs/handbook/image-file-formats.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 9cd65fd48cb..53565dbbdb3 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -552,12 +552,13 @@ JPEG 2000 .. versionadded:: 2.4.0 -Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB`` or -``RGBA`` data. It can also read files containing ``YCbCr`` data, which it -converts on read into ``RGB`` or ``RGBA`` depending on whether or not there is -an alpha channel. Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), -as well as boxed JPEG 2000 files (``.j2p`` or ``.jpx`` files). Pillow does -*not* support files whose components have different sampling frequencies. +Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, +``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to +``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. +Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, +``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports +JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files +(``.jp2`` or ``.jpx`` files). When loading, if you set the ``mode`` on the image prior to the :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to From e9252a9353561d54a5554c2bb873c3264be26add Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 11:07:53 +1100 Subject: [PATCH 015/131] Always return None from compile() --- src/PIL/FontFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 9621770e2a9..04616929a3c 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -65,7 +65,7 @@ def compile(self): ysize = lines * h if xsize == 0 and ysize == 0: - return "" + return self.ysize = h From 85818cd61677cb51c5c70e2ffc2ba7626408ba4b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 11:36:16 +1100 Subject: [PATCH 016/131] Added type hints to FontFile and subclasses --- src/PIL/BdfFontFile.py | 21 +++++++++++++----- src/PIL/FontFile.py | 50 +++++++++++++++++++++++++++++++----------- src/PIL/Image.py | 10 ++++----- src/PIL/PcfFontFile.py | 31 ++++++++++++++------------ src/PIL/_binary.py | 6 ++--- 5 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index b12ddc2d4b4..e3eda4fe98c 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -22,6 +22,8 @@ """ from __future__ import annotations +from typing import BinaryIO + from . import FontFile, Image bdf_slant = { @@ -36,7 +38,17 @@ bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} -def bdf_char(f): +def bdf_char( + f: BinaryIO, +) -> ( + tuple[ + str, + int, + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]], + Image.Image, + ] + | None +): # skip to STARTCHAR while True: s = f.readline() @@ -56,13 +68,12 @@ def bdf_char(f): props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") # load bitmap - bitmap = [] + bitmap = bytearray() while True: s = f.readline() if not s or s[:7] == b"ENDCHAR": break - bitmap.append(s[:-1]) - bitmap = b"".join(bitmap) + bitmap += s[:-1] # The word BBX # followed by the width in x (BBw), height in y (BBh), @@ -92,7 +103,7 @@ def bdf_char(f): class BdfFontFile(FontFile.FontFile): """Font file plugin for the X11 BDF format.""" - def __init__(self, fp): + def __init__(self, fp: BinaryIO): super().__init__() s = fp.readline() diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 04616929a3c..9f643777953 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -16,13 +16,16 @@ from __future__ import annotations import os +from typing import BinaryIO from . import Image, _binary WIDTH = 800 -def puti16(fp, values): +def puti16( + fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int] +) -> None: """Write network order (big-endian) 16-bit sequence""" for v in values: if v < 0: @@ -33,16 +36,34 @@ def puti16(fp, values): class FontFile: """Base class for raster font file handlers.""" - bitmap = None - - def __init__(self): - self.info = {} - self.glyph = [None] * 256 - - def __getitem__(self, ix): + bitmap: Image.Image | None = None + + def __init__(self) -> None: + self.info: dict[bytes, bytes | int] = {} + self.glyph: list[ + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ] = [None] * 256 + + def __getitem__( + self, ix: int + ) -> ( + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ): return self.glyph[ix] - def compile(self): + def compile(self) -> None: """Create metrics and bitmap""" if self.bitmap: @@ -51,7 +72,7 @@ def compile(self): # create bitmap large enough to hold all data h = w = maxwidth = 0 lines = 1 - for glyph in self: + for glyph in self.glyph: if glyph: d, dst, src, im = glyph h = max(h, src[3] - src[1]) @@ -71,7 +92,10 @@ def compile(self): # paste glyphs into bitmap self.bitmap = Image.new("1", (xsize, ysize)) - self.metrics = [None] * 256 + self.metrics: list[ + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]] + | None + ] = [None] * 256 x = y = 0 for i in range(256): glyph = self[i] @@ -88,7 +112,7 @@ def compile(self): self.bitmap.paste(im.crop(src), s) self.metrics[i] = d, dst, s - def save(self, filename): + def save(self, filename: str) -> None: """Save font""" self.compile() @@ -104,6 +128,6 @@ def save(self, filename): for id in range(256): m = self.metrics[id] if not m: - puti16(fp, [0] * 10) + puti16(fp, (0,) * 10) else: puti16(fp, m[0] + m[1] + m[2]) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d04801cba75..045a06080e9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1194,7 +1194,7 @@ def copy(self) -> Image: __copy__ = copy - def crop(self, box=None): + def crop(self, box=None) -> Image: """ Returns a rectangular region from this image. The box is a 4-tuple defining the left, upper, right, and lower pixel @@ -1659,7 +1659,7 @@ def entropy(self, mask=None, extrema=None): return self.im.entropy(extrema) return self.im.entropy() - def paste(self, im, box=None, mask=None): + def paste(self, im, box=None, mask=None) -> None: """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the @@ -2352,7 +2352,7 @@ def transform(x, y, matrix): (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor ) - def save(self, fp, format=None, **params): + def save(self, fp, format=None, **params) -> None: """ Saves this image under the given filename. If no format is specified, the format to use is determined from the filename @@ -2903,7 +2903,7 @@ def _check_size(size): return True -def new(mode, size, color=0): +def new(mode, size, color=0) -> Image: """ Creates a new image with the given mode and size. @@ -2942,7 +2942,7 @@ def new(mode, size, color=0): return im._new(core.fill(mode, size, color)) -def frombytes(mode, size, data, decoder_name="raw", *args): +def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: """ Creates a copy of an image memory from pixel data in a buffer. diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index d602a163349..eff846c7427 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -18,6 +18,7 @@ from __future__ import annotations import io +from typing import BinaryIO, Callable from . import FontFile, Image from ._binary import i8 @@ -49,7 +50,7 @@ ] -def sz(s, o): +def sz(s: bytes, o: int) -> bytes: return s[o : s.index(b"\0", o)] @@ -58,7 +59,7 @@ class PcfFontFile(FontFile.FontFile): name = "name" - def __init__(self, fp, charset_encoding="iso8859-1"): + def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"): self.charset_encoding = charset_encoding magic = l32(fp.read(4)) @@ -104,7 +105,9 @@ def __init__(self, fp, charset_encoding="iso8859-1"): bitmaps[ix], ) - def _getformat(self, tag): + def _getformat( + self, tag: int + ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]: format, size, offset = self.toc[tag] fp = self.fp @@ -119,7 +122,7 @@ def _getformat(self, tag): return fp, format, i16, i32 - def _load_properties(self): + def _load_properties(self) -> dict[bytes, bytes | int]: # # font properties @@ -138,18 +141,16 @@ def _load_properties(self): data = fp.read(i32(fp.read(4))) for k, s, v in p: - k = sz(data, k) - if s: - v = sz(data, v) - properties[k] = v + property_value: bytes | int = sz(data, v) if s else v + properties[sz(data, k)] = property_value return properties - def _load_metrics(self): + def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]: # # font metrics - metrics = [] + metrics: list[tuple[int, int, int, int, int, int, int, int]] = [] fp, format, i16, i32 = self._getformat(PCF_METRICS) @@ -182,7 +183,9 @@ def _load_metrics(self): return metrics - def _load_bitmaps(self, metrics): + def _load_bitmaps( + self, metrics: list[tuple[int, int, int, int, int, int, int, int]] + ) -> list[Image.Image]: # # bitmap data @@ -207,7 +210,7 @@ def _load_bitmaps(self, metrics): data = fp.read(bitmapsize) - pad = BYTES_PER_ROW[padindex] + pad: Callable[[int], int] = BYTES_PER_ROW[padindex] mode = "1;R" if bitorder: mode = "1" @@ -222,7 +225,7 @@ def _load_bitmaps(self, metrics): return bitmaps - def _load_encoding(self): + def _load_encoding(self) -> list[int | None]: fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) @@ -233,7 +236,7 @@ def _load_encoding(self): nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) # map character code to bitmap index - encoding = [None] * min(256, nencoding) + encoding: list[int | None] = [None] * min(256, nencoding) encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index c60c9cec1b7..9bb4260a464 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,7 +18,7 @@ from struct import pack, unpack_from -def i8(c): +def i8(c) -> int: return c if c.__class__ is int else c[0] @@ -57,7 +57,7 @@ def si16be(c, o=0): return unpack_from(">h", c, o)[0] -def i32le(c, o=0): +def i32le(c, o=0) -> int: """ Converts a 4-bytes (32 bits) string to an unsigned integer. @@ -94,7 +94,7 @@ def o32le(i): return pack(" bytes: return pack(">H", i) From 6e97dd5cecc074a0d8622e4aa5658b4263324760 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 12:32:35 +1100 Subject: [PATCH 017/131] Raise ValueError when trying to save without bitmap --- Tests/test_fontfile.py | 12 ++++++++++++ src/PIL/FontFile.py | 3 +++ 2 files changed, 15 insertions(+) create mode 100644 Tests/test_fontfile.py diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py new file mode 100644 index 00000000000..ce1e02f63e0 --- /dev/null +++ b/Tests/test_fontfile.py @@ -0,0 +1,12 @@ +from __future__ import annotations +import pytest + +from PIL import FontFile + + +def test_save(tmp_path): + tempname = str(tmp_path / "temp.pil") + + font = FontFile.FontFile() + with pytest.raises(ValueError): + font.save(tempname) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 9f643777953..3ec1ae819fc 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -118,6 +118,9 @@ def save(self, filename: str) -> None: self.compile() # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") # font metrics From 0aebd577ea08b668f738a735b55fb6c69e8c9332 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 22:27:20 +1100 Subject: [PATCH 018/131] Moved type hint to BYTES_PER_ROW --- 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 eff846c7427..0d1968b140a 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -42,7 +42,7 @@ PCF_GLYPH_NAMES = 1 << 7 PCF_BDF_ACCELERATORS = 1 << 8 -BYTES_PER_ROW = [ +BYTES_PER_ROW: list[Callable[[int], int]] = [ lambda bits: ((bits + 7) >> 3), lambda bits: ((bits + 15) >> 3) & ~1, lambda bits: ((bits + 31) >> 3) & ~3, @@ -210,7 +210,7 @@ def _load_bitmaps( data = fp.read(bitmapsize) - pad: Callable[[int], int] = BYTES_PER_ROW[padindex] + pad = BYTES_PER_ROW[padindex] mode = "1;R" if bitorder: mode = "1" From 6bcf807fe2cb82c859fb757a498edfe5692ededa Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 00:17:57 +0100 Subject: [PATCH 019/131] add type hints for _util --- pyproject.toml | 3 +++ src/PIL/_util.py | 23 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 193e8c9b247..0c25edeb308 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ classifiers = [ dynamic = [ "version", ] +dependencies = [ + 'typing-extensions; python_version < "3.10"', +] [project.optional-dependencies] docs = [ "furo", diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 4634d335bba..d11cbfa5860 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,21 +1,36 @@ from __future__ import annotations import os +import sys from pathlib import Path +from typing import Any, NoReturn +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + from typing_extensions import TypeGuard -def is_path(f): + +def is_path(f: Any) -> TypeGuard[bytes | str | Path]: return isinstance(f, (bytes, str, Path)) -def is_directory(f): +def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: """Checks if an object is a string, and that it points to a directory.""" return is_path(f) and os.path.isdir(f) class DeferredError: - def __init__(self, ex): + def __init__(self, ex: BaseException): self.ex = ex - def __getattr__(self, elt): + def __getattr__(self, elt: str) -> NoReturn: raise self.ex + + @staticmethod + def new(ex: BaseException) -> Any: + """ + Creates an object that raises the wrapped exception ``ex`` when used, + and casts it to :py:obj:`~typing.Any` type. + """ + return DeferredError(ex) From 90d5552800692d3c9c467eeef406dab984a50e68 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 00:40:55 +0100 Subject: [PATCH 020/131] use _util.DeferredError.new everywhere --- Tests/test_util.py | 2 +- pyproject.toml | 2 -- src/PIL/Image.py | 2 +- src/PIL/ImageCms.py | 2 +- src/PIL/ImageFont.py | 6 +++--- src/PIL/PyAccess.py | 2 +- src/PIL/_imagingcms.pyi | 5 +++++ src/PIL/_imagingft.pyi | 5 +++++ 8 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 src/PIL/_imagingcms.pyi create mode 100644 src/PIL/_imagingft.pyi diff --git a/Tests/test_util.py b/Tests/test_util.py index 1457d85f795..4a312beb440 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -66,7 +66,7 @@ def test_deferred_error(): # Arrange # Act - thing = _util.DeferredError(ValueError("Some error text")) + thing = _util.DeferredError.new(ValueError("Some error text")) # Assert with pytest.raises(ValueError): diff --git a/pyproject.toml b/pyproject.toml index 0c25edeb308..ef0ff8a8ece 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,9 +144,7 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageCms.py$', '^src/PIL/ImageFile.py$', - '^src/PIL/ImageFont.py$', '^src/PIL/ImageMath.py$', '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d04801cba75..5a2e8541928 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -92,7 +92,7 @@ class DecompressionBombError(Exception): raise ImportError(msg) except ImportError as v: - core = DeferredError(ImportError("The _imaging C module is not installed.")) + core = DeferredError.new(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 9d27f2513a7..643fce830ba 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -28,7 +28,7 @@ # anything in core. from ._util import DeferredError - _imagingcms = DeferredError(ex) + _imagingcms = DeferredError.new(ex) DESCRIPTION = """ pyCMS diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4eccb..41d8fbc17de 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,7 @@ from enum import IntEnum from io import BytesIO from pathlib import Path -from typing import IO +from typing import BinaryIO from . import Image from ._util import is_directory, is_path @@ -53,7 +53,7 @@ class Layout(IntEnum): except ImportError as ex: from ._util import DeferredError - core = DeferredError(ex) + core = DeferredError.new(ex) def _string_length_check(text): @@ -191,7 +191,7 @@ class FreeTypeFont: def __init__( self, - font: bytes | str | Path | IO | None = None, + font: bytes | str | Path | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 23ff154f6cc..07bb712d83e 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -43,7 +43,7 @@ # anything in core. from ._util import DeferredError - FFI = ffi = DeferredError(ex) + FFI = ffi = DeferredError.new(ex) logger = logging.getLogger(__name__) diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_imagingcms.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi new file mode 100644 index 00000000000..b0235555dc5 --- /dev/null +++ b/src/PIL/_imagingft.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... From cc51dace35bd6e144c3fff05d1b14c78af41c0e0 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 00:41:30 +0100 Subject: [PATCH 021/131] fix types hints for ImageFile._Tile --- pyproject.toml | 1 - src/PIL/ImageFile.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef0ff8a8ece..de707a2838b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,6 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageFile.py$', '^src/PIL/ImageMath.py$', '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index ae4e23db17b..0923979af8b 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -32,7 +32,7 @@ import itertools import struct import sys -from typing import NamedTuple +from typing import Any, NamedTuple from . import Image from ._deprecate import deprecate @@ -94,7 +94,7 @@ class _Tile(NamedTuple): encoder_name: str extents: tuple[int, int, int, int] offset: int - args: tuple | str | None + args: tuple[Any, ...] | str | None # From 3a4298d16c660d5dec46cfa372b6aceb79c1e056 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 14:54:48 +0100 Subject: [PATCH 022/131] avoid hard dependency on typing_extensions --- docs/reference/internal_modules.rst | 13 +++++++++++++ pyproject.toml | 6 +++--- src/PIL/_typing.py | 16 ++++++++++++++++ src/PIL/_util.py | 7 +------ tox.ini | 2 ++ 5 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 src/PIL/_typing.py diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 363a67d9b02..57dd3f35be7 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -25,6 +25,19 @@ Internal Modules :undoc-members: :show-inheritance: +:mod:`~PIL._typing` Module +-------------------------- + +.. module:: PIL._typing + +Provides a convenient way to import type hints that are not available +on some supported Python versions. + +.. py:data:: TypeGuard + :value: typing.TypeGuard + + See :py:obj:`typing.TypeGuard`. + :mod:`~PIL._util` Module ------------------------ diff --git a/pyproject.toml b/pyproject.toml index de707a2838b..abb32f14d82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,6 @@ classifiers = [ dynamic = [ "version", ] -dependencies = [ - 'typing-extensions; python_version < "3.10"', -] [project.optional-dependencies] docs = [ "furo", @@ -68,6 +65,9 @@ tests = [ "pytest-cov", "pytest-timeout", ] +typing = [ + 'typing-extensions; python_version < "3.10"', +] xmp = [ "defusedxml", ] diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py new file mode 100644 index 00000000000..583be41b775 --- /dev/null +++ b/src/PIL/_typing.py @@ -0,0 +1,16 @@ +import sys + +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + try: + from typing_extensions import TypeGuard + except ImportError: + from typing import Any, Type + + class TypeGuard: # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> Type[bool]: + return bool + + +__all__ = ["TypeGuard"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index d11cbfa5860..37cd979f67e 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,14 +1,9 @@ from __future__ import annotations import os -import sys from pathlib import Path from typing import Any, NoReturn - -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - from typing_extensions import TypeGuard +from ._typing import TypeGuard def is_path(f: Any) -> TypeGuard[bytes | str | Path]: diff --git a/tox.ini b/tox.ini index 034d893721b..bfcfaf5d90a 100644 --- a/tox.ini +++ b/tox.ini @@ -37,3 +37,5 @@ deps = numpy commands = mypy src {posargs} +extras = + typing From 0d90bc818789b801170171fe902bc2921850fc11 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Dec 2023 13:57:20 +0000 Subject: [PATCH 023/131] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/_typing.py | 6 ++++-- src/PIL/_util.py | 1 + tox.ini | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 583be41b775..608b2b41fa8 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys if sys.version_info >= (3, 10): @@ -6,10 +8,10 @@ try: from typing_extensions import TypeGuard except ImportError: - from typing import Any, Type + from typing import Any class TypeGuard: # type: ignore[no-redef] - def __class_getitem__(cls, item: Any) -> Type[bool]: + def __class_getitem__(cls, item: Any) -> type[bool]: return bool diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 37cd979f67e..13f369cca1d 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -3,6 +3,7 @@ import os from pathlib import Path from typing import Any, NoReturn + from ._typing import TypeGuard diff --git a/tox.ini b/tox.ini index bfcfaf5d90a..d89d017e45c 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,7 @@ skip_install = true deps = mypy==1.7.1 numpy -commands = - mypy src {posargs} extras = typing +commands = + mypy src {posargs} From 0c767f0d7c2da77edb984f927de6d42848a5fe23 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 5 Oct 2023 21:27:18 +0300 Subject: [PATCH 024/131] Coverage: Don't complain about code that shouldn't run: def create_lut(): --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index f71b6b1a281..d7095dce3a2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,6 +9,8 @@ exclude_lines = # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: + # Don't complain about code that shouldn't run + def create_lut(): # Don't complain about debug code if DEBUG: From 5938423c63f9a71bed4c88c3c1d4d1c9b51a0de0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 12 Nov 2023 12:53:42 +0200 Subject: [PATCH 025/131] Coverage: Use exclude_also instead of exclude_lines --- .coveragerc | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index d7095dce3a2..1c5e7654e98 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,11 +2,8 @@ [report] # Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma: - pragma: no cover - - # Don't complain if non-runnable code isn't run: +exclude_also = + # Don't complain if non-runnable code isn't run if 0: if __name__ == .__main__.: # Don't complain about code that shouldn't run From 9475c46d30ddbefc74afd185ac7f19e6da8a8ca7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 27 Dec 2023 16:58:42 +0200 Subject: [PATCH 026/131] Don't complain about compatibility code: class TypeGuard --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 1c5e7654e98..f18c5ea52b0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,8 @@ exclude_also = def create_lut(): # Don't complain about debug code if DEBUG: + # Don't complain about compatibility code + class TypeGuard: [run] omit = From de381d0efb96096cb9a56c69711635a8e53417b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Wed, 27 Dec 2023 16:17:51 +0100 Subject: [PATCH 027/131] Update docs/reference/internal_modules.rst Co-authored-by: Hugo van Kemenade --- docs/reference/internal_modules.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 57dd3f35be7..f2932c32200 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -31,7 +31,7 @@ Internal Modules .. module:: PIL._typing Provides a convenient way to import type hints that are not available -on some supported Python versions. +on some Python versions. .. py:data:: TypeGuard :value: typing.TypeGuard From 30015f6236ee3165e06b78ff7dced956e21e24d5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 16:16:25 +0100 Subject: [PATCH 028/131] simplify decompression bomb check in FreeTypeFont.render --- src/PIL/ImageFont.py | 17 +++-------------- src/_imagingft.c | 14 ++++---------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4eccb..c8d834f91d4 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -582,22 +582,13 @@ def getmask2( _string_length_check(text) if start is None: start = (0, 0) - im = None - size = None def fill(width, height): - nonlocal im, size - size = (width, height) - if Image.MAX_IMAGE_PIXELS is not None: - pixels = max(1, width) * max(1, height) - if pixels > 2 * Image.MAX_IMAGE_PIXELS: - return - - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - return im + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - offset = self.font.render( + return self.font.render( text, fill, mode, @@ -610,8 +601,6 @@ def fill(width, height): start[0], start[1], ) - Image._decompression_bomb_check(size) - return im, offset def font_variant( self, font=None, size=None, index=None, encoding=None, layout_engine=None diff --git a/src/_imagingft.c b/src/_imagingft.c index 68c66ac2c60..6e24fcf95ed 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { image = PyObject_CallFunction(fill, "ii", width, height); if (image == Py_None) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", 0, 0); + return Py_BuildValue("N(ii)", image, 0, 0); } else if (image == NULL) { PyMem_Del(glyph_info); return NULL; @@ -894,7 +894,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("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); } if (stroke_width) { @@ -1130,18 +1130,12 @@ 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("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); glyph_error: - if (im->destroy) { - im->destroy(im); - } - if (im->image) { - free(im->image); - } + Py_DECREF(image); if (stroker != NULL) { FT_Done_Glyph(glyph); } From a16974e24028667330975d61ea0229481af5e5d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Dec 2023 21:07:16 +1100 Subject: [PATCH 029/131] Restored testing of ImageFont class --- Tests/test_imagefont.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6e04cddc748..efe5236435d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1053,11 +1053,11 @@ def test_too_many_characters(font): with pytest.raises(ValueError): transposed_font.getlength("A" * 1_000_001) - default_font = ImageFont.load_default() + imagefont = ImageFont.ImageFont() with pytest.raises(ValueError): - default_font.getlength("A" * 1_000_001) + imagefont.getlength("A" * 1_000_001) with pytest.raises(ValueError): - default_font.getbbox("A" * 1_000_001) + imagefont.getbbox("A" * 1_000_001) @pytest.mark.parametrize( From 372083c59f06c37564be040a1d4b8cd1dc5bd0ac Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Thu, 28 Dec 2023 13:00:17 -0600 Subject: [PATCH 030/131] Check similarity of round-tripped YCbCr JPEG, for symmetry with RGB --- Tests/test_file_jpeg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 75851e4773e..5513b140336 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -148,6 +148,7 @@ def getchannels(im): im = hopper() im_ycbcr = self.roundtrip(im) assert getchannels(im_ycbcr) == (1, 2, 3) + assert_image_similar(im, im_ycbcr, 17) im_rgb = self.roundtrip(im, keep_rgb=True) assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B")) From a5e42107eadd9e1cdcaf075782338f72b9142e32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 11:23:34 +1100 Subject: [PATCH 031/131] Removed __future__ import from fuzz_font and fuzz_pillow --- Tests/oss-fuzz/fuzz_font.py | 2 -- Tests/oss-fuzz/fuzz_pillow.py | 2 -- pyproject.toml | 2 ++ 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index 024117c56d0..bc2ba9a7e27 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 -from __future__ import annotations - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index c1ab42e5651..545daccb680 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 -from __future__ import annotations - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/pyproject.toml b/pyproject.toml index 193e8c9b247..6e26ff4f913 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,8 @@ extend-ignore = [ [tool.ruff.per-file-ignores] "Tests/*.py" = ["I001"] +"Tests/oss-fuzz/fuzz_font.py" = ["I002"] +"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] [tool.ruff.isort] known-first-party = ["PIL"] From f6bcf4e1ae3184ff9ed0cfcb46a0aa80423da537 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 14:15:40 +1100 Subject: [PATCH 032/131] Use IMAGEWIDTH and IMAGELENGTH when calculating strip size --- Tests/test_file_tiff.py | 25 +++++++++++++++++++++++-- Tests/test_file_tiff_metadata.py | 1 + src/PIL/TiffImagePlugin.py | 13 +++++++------ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 0851796d000..73a1223d75c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -484,13 +484,13 @@ def test_modify_exif(self, tmp_path): outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() - exif[256] = 100 + exif[264] = 100 im.save(outfile, exif=exif) with Image.open(outfile) as im: exif = im.getexif() - assert exif[256] == 100 + assert exif[264] == 100 def test_reload_exif_after_seek(self): with Image.open("Tests/images/multipage.tiff") as im: @@ -773,6 +773,27 @@ def test_get_photoshop_blocks(self): 4001, ] + def test_tiff_chunks(self, tmp_path): + tmpfile = str(tmp_path / "temp.tif") + + im = hopper() + with open(tmpfile, "wb") as fp: + for y in range(0, 128, 32): + chunk = im.crop((0, y, 128, y + 32)) + if y == 0: + chunk.save( + fp, + "TIFF", + tiffinfo={ + TiffImagePlugin.IMAGEWIDTH: 128, + TiffImagePlugin.IMAGELENGTH: 128, + }, + ) + else: + fp.write(chunk.tobytes()) + + assert_image_equal_tofile(im, tmpfile) + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index edd57e6b54d..ed90031fa50 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -162,6 +162,7 @@ def test_change_stripbytecounts_tag_type(tmp_path): # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT im = im.resize((500, 500)) + info[TiffImagePlugin.IMAGEWIDTH] = im.width # STRIPBYTECOUNTS can be a SHORT or a LONG info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fc242ca64c2..f9da3e6495e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1704,25 +1704,26 @@ def _save(im, fp, filename): colormap += [0] * (256 - colors) ifd[COLORMAP] = colormap # data orientation - stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) + w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] + stride = len(bits) * ((w * bits[0] + 7) // 8) # aim for given strip size (64 KB by default) when using libtiff writer if libtiff: im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) - rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) + rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) # JPEG encoder expects multiple of 8 rows if compression == "jpeg": - rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) else: - rows_per_strip = im.size[1] + rows_per_strip = h if rows_per_strip == 0: rows_per_strip = 1 strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip - strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip + strips_per_image = (h + rows_per_strip - 1) // rows_per_strip ifd[ROWSPERSTRIP] = rows_per_strip if strip_byte_counts >= 2**16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( - stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), + stride * h - strip_byte_counts * (strips_per_image - 1), ) ifd[STRIPOFFSETS] = tuple( range(0, strip_byte_counts * strips_per_image, strip_byte_counts) From 3c7603b57dc60cd6a052bfb4d1fbf1c5ec7b6ff8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 22:05:31 +1100 Subject: [PATCH 033/131] Trusted PyPI publishing is now in use in GitHub Actions --- RELEASING.md | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 74f427f0376..b3fd72a520e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -20,12 +20,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. git tag 5.2.0 git push --tags ``` -* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) -* [ ] Check and upload all source and binary distributions e.g.: - ```bash - python3 -m twine check --strict dist/* - python3 -m twine upload dist/Pillow-5.2.0* - ``` +* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] 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: @@ -55,12 +50,7 @@ Released as needed for security, installation or critical bug fixes. ```bash make sdist ``` -* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) -* [ ] Check and upload all source and binary distributions e.g.: - ```bash - python3 -m twine check --strict dist/* - python3 -m twine upload dist/Pillow-5.2.1* - ``` +* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: ```bash git push @@ -82,11 +72,7 @@ Released as needed privately to individual vendors for critical security-related git tag 2.5.3 git push origin --tags ``` -* [ ] Create and check source distribution: - ```bash - make sdist - ``` -* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) +* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: ```bash git push origin 2.5.x @@ -94,14 +80,15 @@ Released as needed privately to individual vendors for critical security-related ## Source and Binary Distributions -* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) - and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): +* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) + has passed, including the "Upload release to PyPI" job. This will have been triggered + by the new tag. +* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) + and copy into `dist`. Check and upload them e.g.: ```bash - gh run download --dir dist - # select dist + python3 -m twine check --strict dist/* + python3 -m twine upload dist/Pillow-5.2.0* ``` -* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) - and copy into `dist`. ## Publicize Release From 9c7ff4c86d2be8750fb33f2372c1c9bc9c20e835 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 29 Dec 2023 13:25:54 +0200 Subject: [PATCH 034/131] Add 'Type hints' as a release note category --- .github/release-drafter.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 4d855469a12..3711d91f0d5 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -13,6 +13,8 @@ categories: label: "Removal" - title: "Testing" label: "Testing" + - title: "Type hints" + label: "Type hints" exclude-labels: - "changelog: skip" From f7ec665bf1656245e9e3b483ff92e9eb1e0ef465 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 22:59:43 +1100 Subject: [PATCH 035/131] Support setting ROWSPERSTRIP tag --- Tests/test_file_tiff.py | 8 ++++++++ Tests/test_file_tiff_metadata.py | 2 ++ src/PIL/TiffImagePlugin.py | 31 +++++++++++++++++-------------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 0851796d000..0737c8a2af2 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -612,6 +612,14 @@ def test_roundtrip_tiff_uint16(self, tmp_path): assert_image_equal_tofile(im, tmpfile) + def test_rowsperstrip(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + im = hopper() + im.save(outfile, tiffinfo={278: 256}) + + with Image.open(outfile) as im: + assert im.tag_v2[278] == 256 + def test_strip_raw(self): infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index edd57e6b54d..1a814d97eb9 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -123,6 +123,7 @@ def test_write_metadata(tmp_path): """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: f = str(tmp_path / "temp.tiff") + del img.tag[278] img.save(f, tiffinfo=img.tag) original = img.tag_v2.named() @@ -159,6 +160,7 @@ def test_change_stripbytecounts_tag_type(tmp_path): out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.tif") as im: info = im.tag_v2 + del info[278] # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT im = im.resize((500, 500)) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fc242ca64c2..e9db64e5fc5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1705,20 +1705,23 @@ def _save(im, fp, filename): ifd[COLORMAP] = colormap # data orientation stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) - # aim for given strip size (64 KB by default) when using libtiff writer - if libtiff: - im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) - rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) - # JPEG encoder expects multiple of 8 rows - if compression == "jpeg": - rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) - else: - rows_per_strip = im.size[1] - if rows_per_strip == 0: - rows_per_strip = 1 - strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip - strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip - ifd[ROWSPERSTRIP] = rows_per_strip + if ROWSPERSTRIP not in ifd: + # aim for given strip size (64 KB by default) when using libtiff writer + if libtiff: + im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) + rows_per_strip = ( + 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) + ) + # JPEG encoder expects multiple of 8 rows + if compression == "jpeg": + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) + else: + rows_per_strip = im.size[1] + if rows_per_strip == 0: + rows_per_strip = 1 + ifd[ROWSPERSTRIP] = rows_per_strip + strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] + strips_per_image = (im.size[1] + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] if strip_byte_counts >= 2**16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( From 6a33d6d170d51419ea63e905637c98f5e6db1af9 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 29 Dec 2023 23:15:41 +0100 Subject: [PATCH 036/131] add type hints to PIL._binary --- src/PIL/_binary.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 9bb4260a464..b8b871df678 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,16 +18,16 @@ from struct import pack, unpack_from -def i8(c) -> int: - return c if c.__class__ is int else c[0] +def i8(c: int | bytes) -> int: + return c if c.__class__ is int else c[0] # type: ignore[index, return-value] -def o8(i): +def o8(i: int) -> bytes: return bytes((i & 255,)) # Input, le = little endian, be = big endian -def i16le(c, o=0): +def i16le(c: bytes, o: int = 0) -> int: """ Converts a 2-bytes (16 bits) string to an unsigned integer. @@ -37,7 +37,7 @@ def i16le(c, o=0): return unpack_from(" int: """ Converts a 2-bytes (16 bits) string to a signed integer. @@ -47,7 +47,7 @@ def si16le(c, o=0): return unpack_from(" int: """ Converts a 2-bytes (16 bits) string to a signed integer, big endian. @@ -57,7 +57,7 @@ def si16be(c, o=0): return unpack_from(">h", c, o)[0] -def i32le(c, o=0) -> int: +def i32le(c: bytes, o: int = 0) -> int: """ Converts a 4-bytes (32 bits) string to an unsigned integer. @@ -67,7 +67,7 @@ def i32le(c, o=0) -> int: return unpack_from(" int: """ Converts a 4-bytes (32 bits) string to a signed integer. @@ -77,26 +77,26 @@ def si32le(c, o=0): return unpack_from(" int: return unpack_from(">H", c, o)[0] -def i32be(c, o=0): +def i32be(c: bytes, o: int = 0) -> int: return unpack_from(">I", c, o)[0] # Output, le = little endian, be = big endian -def o16le(i): +def o16le(i: int) -> bytes: return pack(" bytes: return pack(" bytes: +def o16be(i: int) -> bytes: return pack(">H", i) -def o32be(i): +def o32be(i: int) -> bytes: return pack(">I", i) From 9a6b6316a781b27c9f845fd71c6e0290e8b4601b Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 29 Dec 2023 23:12:30 +0100 Subject: [PATCH 037/131] add type hints to PIL.ContainerIO --- src/PIL/ContainerIO.py | 23 ++++++++++++----------- src/PIL/TarIO.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 64d04242639..0035296a45c 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -16,15 +16,16 @@ from __future__ import annotations import io +from typing import IO, AnyStr, Generic, Literal -class ContainerIO: +class ContainerIO(Generic[AnyStr]): """ A file object that provides read access to a part of an existing file (for example a TAR file). """ - def __init__(self, file, offset, length) -> None: + def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: """ Create file object. @@ -32,7 +33,7 @@ def __init__(self, file, offset, length) -> None: :param offset: Start of region, in bytes. :param length: Size of region, in bytes. """ - self.fh = file + self.fh: IO[AnyStr] = file self.pos = 0 self.offset = offset self.length = length @@ -41,10 +42,10 @@ def __init__(self, file, offset, length) -> None: ## # Always false. - def isatty(self): + def isatty(self) -> bool: return False - def seek(self, offset, mode=io.SEEK_SET): + def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: """ Move file pointer. @@ -63,7 +64,7 @@ def seek(self, offset, mode=io.SEEK_SET): self.pos = max(0, min(self.pos, self.length)) self.fh.seek(self.offset + self.pos) - def tell(self): + def tell(self) -> int: """ Get current file pointer. @@ -71,7 +72,7 @@ def tell(self): """ return self.pos - def read(self, n=0): + def read(self, n: int = 0) -> AnyStr: """ Read data. @@ -84,17 +85,17 @@ def read(self, n=0): else: n = self.length - self.pos if not n: # EOF - return b"" if "b" in self.fh.mode else "" + return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] self.pos = self.pos + n return self.fh.read(n) - def readline(self): + def readline(self) -> AnyStr: """ Read a line of text. :returns: An 8-bit string. """ - s = b"" if "b" in self.fh.mode else "" + s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] newline_character = b"\n" if "b" in self.fh.mode else "\n" while True: c = self.read(1) @@ -105,7 +106,7 @@ def readline(self): break return s - def readlines(self): + def readlines(self) -> list[AnyStr]: """ Read multiple lines of text. diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index c9923487d5e..7470663b4a1 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -21,7 +21,7 @@ from . import ContainerIO -class TarIO(ContainerIO.ContainerIO): +class TarIO(ContainerIO.ContainerIO[bytes]): """A file object that provides read access to a given member of a TAR file.""" def __init__(self, tarfile: str, file: str) -> None: From 45c726fd4daa63236a8f3653530f297dc87b160a Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 27 Oct 2023 11:21:18 +0200 Subject: [PATCH 038/131] Don't allow __ or builtins in env dictionarys for ImageMath.eval --- src/PIL/ImageMath.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 7ca512e7568..cf108e2586f 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -237,6 +237,10 @@ def eval(expression, _dict={}, **kw): args.update(_dict) args.update(kw) for k, v in args.items(): + if '__' in k or hasattr(__builtins__, k): + msg = f"'{k}' not allowed" + raise ValueError(msg) + if hasattr(v, "im"): args[k] = _Operand(v) From 0ca3c33c59927e1c7e0c14dbc1eea1dfb2431a80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Oct 2023 15:58:52 +1100 Subject: [PATCH 039/131] Allow ops --- Tests/test_imagemath.py | 5 +++++ src/PIL/ImageMath.py | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 22de86c7cab..9a0326ece3b 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -64,6 +64,11 @@ def test_prevent_exec(expression): ImageMath.eval(expression) +def test_prevent_double_underscores(): + with pytest.raises(ValueError): + ImageMath.eval("1", {"__": None}) + + def test_logical(): assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index cf108e2586f..fd7d78d4583 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -234,13 +234,14 @@ def eval(expression, _dict={}, **kw): # build execution namespace args = ops.copy() - args.update(_dict) - args.update(kw) - for k, v in args.items(): - if '__' in k or hasattr(__builtins__, k): + for k in list(_dict.keys()) + list(kw.keys()): + if "__" in k or hasattr(__builtins__, k): msg = f"'{k}' not allowed" raise ValueError(msg) + args.update(_dict) + args.update(kw) + for k, v in args.items(): if hasattr(v, "im"): args[k] = _Operand(v) From 557ba59d13de919d04b3fd4cdef8634f7d4b3348 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Dec 2023 09:30:12 +1100 Subject: [PATCH 040/131] Include further builtins --- Tests/test_imagemath.py | 5 +++++ docs/releasenotes/10.2.0.rst | 9 ++++++--- src/PIL/ImageMath.py | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 9a0326ece3b..9281de6f66a 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -69,6 +69,11 @@ def test_prevent_double_underscores(): ImageMath.eval("1", {"__": None}) +def test_prevent_builtins(): + with pytest.raises(ValueError): + ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None}) + + def test_logical(): assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 9883f10baf3..ade152fcd59 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -62,10 +62,13 @@ output only the quantization and Huffman tables for the image. Security ======== -TODO -^^^^ +Restricted environment keys for ImageMath.eval +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:cve:`2023-50447`: If an attacker has control over the keys passed to the +``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute +arbitrary code. To prevent this, keys matching the names of builtins and keys +containing double underscores will now raise a :py:exc:`ValueError`. Other Changes ============= diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index fd7d78d4583..b77f4bce567 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -235,7 +235,7 @@ def eval(expression, _dict={}, **kw): # build execution namespace args = ops.copy() for k in list(_dict.keys()) + list(kw.keys()): - if "__" in k or hasattr(__builtins__, k): + if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" raise ValueError(msg) From aaf99d18ae3e25369b1cd4cebcf51e20a9b6d1ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Dec 2023 10:38:09 +1100 Subject: [PATCH 041/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index df4e11e0e48..c5c3ea9c774 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Restricted environment keys for ImageMath.eval() #7655 + [wiredfool, radarhere] + - Fix incorrect color blending for overlapping glyphs #7497 [ZachNagengast, nulano, radarhere] From 9158c9aec029bb4aaa2fca7040f04ff030536a0f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 26 Dec 2023 19:03:18 +0200 Subject: [PATCH 042/131] Optimise ImageColor using functools.lru_cache --- src/PIL/ImageColor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index bfad27c82d6..ad59b066779 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -19,10 +19,12 @@ from __future__ import annotations import re +from functools import lru_cache from . import Image +@lru_cache def getrgb(color): """ Convert a color string to an RGB or RGBA tuple. If the string cannot be @@ -121,6 +123,7 @@ def getrgb(color): raise ValueError(msg) +@lru_cache def getcolor(color, mode): """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if From ee6e12a8032a8255b2f7a60a40c50377c17a03fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 18:37:16 +1100 Subject: [PATCH 043/131] 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 cf65e86d734..0069eb5bcec 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 Jeffrey A. Clark (Alex) and contributors. + Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors. Like PIL, Pillow is licensed under the open source HPND License: diff --git a/docs/COPYING b/docs/COPYING index bc44ba388a6..73af6d99c0f 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 Jeffrey A. Clark (Alex) and contributors + Copyright © 2010-2024 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 a70dece7469..9ae7ae605fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,7 +54,7 @@ # General information about the project. project = "Pillow (PIL Fork)" copyright = ( - "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" + "1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors" ) author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" From 1d9c931626b3cd08b9fa19c9e9e2d8c79604046e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 22:43:08 +1100 Subject: [PATCH 044/131] Changed tile tuple to match other plugins --- Tests/test_file_iptc.py | 9 +++++++++ src/PIL/IptcImagePlugin.py | 10 +++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index d0ecde393a4..e1a8c92c713 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,6 +11,15 @@ TEST_FILE = "Tests/images/iptc.jpg" +def test_open(): + f = BytesIO( + b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c" + b"\x03\x14\x00\x01\x01\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x00" + ) + with Image.open(f) as im: + assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] + + def test_getiptcinfo_jpg_none(): # Arrange with hopper() as im: diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index e7dc3e4e4d2..09a60f25c40 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -128,24 +128,20 @@ def _open(self): # tile if tag == (8, 10): - self.tile = [ - ("iptc", (compression, offset), (0, 0, self.size[0], self.size[1])) - ] + self.tile = [("iptc", (0, 0) + self.size, offset, compression)] def load(self): if len(self.tile) != 1 or self.tile[0][0] != "iptc": return ImageFile.ImageFile.load(self) - type, tile, box = self.tile[0] - - encoding, offset = tile + offset, compression = self.tile[0][2:] self.fp.seek(offset) # Copy image data to temporary file o_fd, outfile = tempfile.mkstemp(text=False) o = os.fdopen(o_fd) - if encoding == "raw": + if compression == "raw": # To simplify access to the extracted file, # prepend a PPM header o.write("P5\n%d %d\n255\n" % self.size) From 2ec53e36e9135fda4e2eb6bbd5343a7facae71da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 23:17:28 +1100 Subject: [PATCH 045/131] Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() --- Tests/test_imagefont.py | 2 ++ docs/releasenotes/10.2.0.rst | 15 +++++++++++++-- src/PIL/ImageFont.py | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index efe5236435d..807d581edf0 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1058,6 +1058,8 @@ def test_too_many_characters(font): imagefont.getlength("A" * 1_000_001) with pytest.raises(ValueError): imagefont.getbbox("A" * 1_000_001) + with pytest.raises(ValueError): + imagefont.getmask("A" * 1_000_001) @pytest.mark.parametrize( diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index ade152fcd59..6ab139b560b 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -62,8 +62,19 @@ output only the quantization and Huffman tables for the image. Security ======== -Restricted environment keys for ImageMath.eval -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using arbitrary strings as text input, +Pillow will now raise a :py:exc:`ValueError` if the number of characters passed into +:py:meth:`PIL.ImageFont.ImageFont.getmask` 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``. + +ImageMath.eval: Restricted environment keys +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :cve:`2023-50447`: If an attacker has control over the keys passed to the ``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4eccb..7f0366ddb5d 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -149,6 +149,7 @@ def getmask(self, text, mode="", *args, **kwargs): :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ + _string_length_check(text) return self.font.getmask(text, mode) def getbbox(self, text, *args, **kwargs): From 46a6ddf0c2eb36a06f9952a4e8d98ff2183198f6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 31 Dec 2023 13:47:37 +0100 Subject: [PATCH 046/131] fix loading IPTC images and add test --- Tests/test_file_iptc.py | 9 ++++++--- src/PIL/IptcImagePlugin.py | 21 ++++++--------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index e1a8c92c713..04313ed8530 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -6,18 +6,21 @@ from PIL import Image, IptcImagePlugin -from .helper import hopper +from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/iptc.jpg" def test_open(): + expected = Image.new("L", (1, 1), 0) + f = BytesIO( - b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c" - b"\x03\x14\x00\x01\x01\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x00" + b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" + b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00" ) with Image.open(f) as im: assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] + assert_image_equal(im, expected) def test_getiptcinfo_jpg_none(): diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 09a60f25c40..7b6123c667f 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -16,8 +16,7 @@ # from __future__ import annotations -import os -import tempfile +from io import BytesIO from . import Image, ImageFile from ._binary import i8, o8 @@ -139,12 +138,11 @@ def load(self): self.fp.seek(offset) # Copy image data to temporary file - o_fd, outfile = tempfile.mkstemp(text=False) - o = os.fdopen(o_fd) + o = BytesIO() if compression == "raw": # To simplify access to the extracted file, # prepend a PPM header - o.write("P5\n%d %d\n255\n" % self.size) + o.write(b"P5\n%d %d\n255\n" % self.size) while True: type, size = self.field() if type != (8, 10): @@ -155,17 +153,10 @@ def load(self): break o.write(s) size -= len(s) - o.close() - try: - with Image.open(outfile) as _im: - _im.load() - self.im = _im.im - finally: - try: - os.unlink(outfile) - except OSError: - pass + with Image.open(o) as _im: + _im.load() + self.im = _im.im Image.register_open(IptcImageFile.format, IptcImageFile) From f410ec4eab314653bcbc91a9d0ab754fa331972e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 00:21:25 +1100 Subject: [PATCH 047/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c5c3ea9c774..1dc8e9aaaa6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,9 +5,18 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662 + [radarhere] + +- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657 + [hugovk] + - Restricted environment keys for ImageMath.eval() #7655 [wiredfool, radarhere] +- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641 + [hugovk, radarhere] + - Fix incorrect color blending for overlapping glyphs #7497 [ZachNagengast, nulano, radarhere] From b1e88ac17fcde6286a9b50262ca945fbf90652c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sun, 31 Dec 2023 14:49:48 +0100 Subject: [PATCH 048/131] omit default color value Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_iptc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 04313ed8530..075a461af18 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -12,7 +12,7 @@ def test_open(): - expected = Image.new("L", (1, 1), 0) + expected = Image.new("L", (1, 1)) f = BytesIO( b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" From 3396ce102d0a2e8db663522001cab9786edf19c0 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 29 Dec 2023 23:18:08 +0100 Subject: [PATCH 049/131] do not accept int in PIL._binary.i8 --- src/PIL/IptcImagePlugin.py | 15 +++++++++------ src/PIL/_binary.py | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index e7dc3e4e4d2..cc39441b6d5 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -20,26 +20,29 @@ import tempfile from . import Image, ImageFile -from ._binary import i8, o8 from ._binary import i16be as i16 from ._binary import i32be as i32 COMPRESSION = {1: "raw", 5: "jpeg"} -PAD = o8(0) * 4 +PAD = b"\0\0\0\0" # # Helpers +def _i8(c: int | bytes) -> int: + return c if isinstance(c, int) else c[0] + + def i(c): return i32((PAD + c)[-4:]) def dump(c): for i in c: - print("%02x" % i8(i), end=" ") + print("%02x" % _i8(i), end=" ") print() @@ -103,10 +106,10 @@ def _open(self): self.info[tag] = tagdata # mode - layers = i8(self.info[(3, 60)][0]) - component = i8(self.info[(3, 60)][1]) + layers = self.info[(3, 60)][0] + component = self.info[(3, 60)][1] if (3, 65) in self.info: - id = i8(self.info[(3, 65)][0]) - 1 + id = self.info[(3, 65)][0] - 1 else: id = 0 if layers == 1 and not component: diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index b8b871df678..0a07e8d0e12 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,8 +18,8 @@ from struct import pack, unpack_from -def i8(c: int | bytes) -> int: - return c if c.__class__ is int else c[0] # type: ignore[index, return-value] +def i8(c: bytes) -> int: + return c[0] def o8(i: int) -> bytes: From fa4b3776f0dda812a1cea15f3ba17a3777b8c5b6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 30 Dec 2023 00:02:34 +0100 Subject: [PATCH 050/131] =?UTF-8?q?=EF=BB=BFdeprecate=20IptcImagePlugin.{d?= =?UTF-8?q?ump,i,PAD}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/test_file_iptc.py | 20 ++++++++++++-------- src/PIL/IptcImagePlugin.py | 21 +++++++++++++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index e1a8c92c713..3960e027a93 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -87,24 +87,28 @@ def test_i(): c = b"a" # Act - ret = IptcImagePlugin.i(c) + with pytest.warns(DeprecationWarning): + ret = IptcImagePlugin.i(c) # Assert assert ret == 97 -def test_dump(): +def test_dump(monkeypatch): # Arrange c = b"abc" # Temporarily redirect stdout - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() + mystdout = StringIO() + monkeypatch.setattr(sys, "stdout", mystdout) # Act - IptcImagePlugin.dump(c) - - # Reset stdout - sys.stdout = old_stdout + with pytest.warns(DeprecationWarning): + IptcImagePlugin.dump(c) # Assert assert mystdout.getvalue() == "61 62 63 \n" + + +def test_pad_deprecation(): + with pytest.warns(DeprecationWarning): + assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 3a028de2d47..deac39e25fc 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -22,25 +22,38 @@ from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._deprecate import deprecate COMPRESSION = {1: "raw", 5: "jpeg"} -PAD = b"\0\0\0\0" + +def __getattr__(name): + if name == "PAD": + deprecate("IptcImagePlugin.PAD", 12) + return b"\0\0\0\0" + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) # # Helpers +def _i(c): + return i32((b"\0\0\0\0" + c)[-4:]) + + def _i8(c: int | bytes) -> int: return c if isinstance(c, int) else c[0] def i(c): - return i32((PAD + c)[-4:]) + deprecate("IptcImagePlugin.i", 12) + return _i(c) def dump(c): + deprecate("IptcImagePlugin.dump", 12) for i in c: print("%02x" % _i8(i), end=" ") print() @@ -56,7 +69,7 @@ class IptcImageFile(ImageFile.ImageFile): format_description = "IPTC/NAA" def getint(self, key): - return i(self.info[key]) + return _i(self.info[key]) def field(self): # @@ -80,7 +93,7 @@ def field(self): elif size == 128: size = 0 elif size > 128: - size = i(self.fp.read(size - 128)) + size = _i(self.fp.read(size - 128)) else: size = i16(s, 3) From aa605bc6f2c1baa00d95e083a3be2fe0b5051c04 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 31 Dec 2023 01:25:19 +0100 Subject: [PATCH 051/131] =?UTF-8?q?=EF=BB=BFdocument=20IptcImagePlugin=20d?= =?UTF-8?q?eprecations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/deprecations.rst | 11 +++++++++++ docs/releasenotes/10.2.0.rst | 12 ++++++++---- src/PIL/IptcImagePlugin.py | 2 ++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 75c0b73ebed..a42dc555fea 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -44,6 +44,17 @@ ImageFile.raise_oserror error codes returned by a codec's ``decode()`` method, which ImageFile already does automatically. +IptcImageFile helper functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.2.0 + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). These are undocumented helper functions intended +for internal use, so there is no replacement. They can each be replaced +by a single line of code using builtin functions in Python. + Removed features ---------------- diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 6ab139b560b..0244b6f7792 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -20,10 +20,14 @@ ImageFile.raise_oserror error codes returned by a codec's ``decode()`` method, which ImageFile already does automatically. -TODO -^^^^ - -TODO +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). These are undocumented helper functions intended +for internal use, so there is no replacement. They can each be replaced +by a single line of code using builtin functions in Python. API Changes =========== diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index deac39e25fc..faf3ed936f3 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -48,11 +48,13 @@ def _i8(c: int | bytes) -> int: def i(c): + """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.i", 12) return _i(c) def dump(c): + """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.dump", 12) for i in c: print("%02x" % _i8(i), end=" ") From e1ea522f706ac51f4bdd6b58724af56eebd19d66 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 13:21:56 +0100 Subject: [PATCH 052/131] =?UTF-8?q?=EF=BB=BFAdded=20further=20type=20hints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/PIL/Image.py | 6 +++--- src/PIL/IptcImagePlugin.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 613d9462a60..055dbda7a03 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3190,7 +3190,7 @@ def _decompression_bomb_check(size): ) -def open(fp, mode="r", formats=None): +def open(fp, mode="r", formats=None) -> Image: """ Opens and identifies the given image file. @@ -3415,7 +3415,7 @@ def merge(mode, bands): # Plugin registry -def register_open(id, factory, accept=None): +def register_open(id, factory, accept=None) -> None: """ Register an image file plugin. This function should not be used in application code. @@ -3469,7 +3469,7 @@ def register_save_all(id, driver): SAVE_ALL[id.upper()] = driver -def register_extension(id, extension): +def register_extension(id, extension) -> None: """ Registers an image extension. This function should not be used in application code. diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index faf3ed936f3..2314ddce892 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -18,6 +18,7 @@ import os import tempfile +from typing import Sequence from . import Image, ImageFile from ._binary import i16be as i16 @@ -27,7 +28,7 @@ COMPRESSION = {1: "raw", 5: "jpeg"} -def __getattr__(name): +def __getattr__(name: str) -> bytes: if name == "PAD": deprecate("IptcImagePlugin.PAD", 12) return b"\0\0\0\0" @@ -39,7 +40,7 @@ def __getattr__(name): # Helpers -def _i(c): +def _i(c: bytes) -> int: return i32((b"\0\0\0\0" + c)[-4:]) @@ -47,13 +48,13 @@ def _i8(c: int | bytes) -> int: return c if isinstance(c, int) else c[0] -def i(c): +def i(c: bytes) -> int: """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.i", 12) return _i(c) -def dump(c): +def dump(c: Sequence[int | bytes]) -> None: """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.dump", 12) for i in c: @@ -70,10 +71,10 @@ class IptcImageFile(ImageFile.ImageFile): format = "IPTC" format_description = "IPTC/NAA" - def getint(self, key): + def getint(self, key: tuple[int, int]) -> int: return _i(self.info[key]) - def field(self): + def field(self) -> tuple[tuple[int, int] | None, int]: # # get a IPTC field header s = self.fp.read(5) @@ -101,7 +102,7 @@ def field(self): return tag, size - def _open(self): + def _open(self) -> None: # load descriptive fields while True: offset = self.fp.tell() From 2825323e84aff1c6005506fb1d6e78373a356486 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 31 Dec 2023 16:46:24 +0200 Subject: [PATCH 053/131] Release notes: add ImageColor and ImageMode optimisations, and type hints --- docs/PIL.rst | 9 ++++++++ docs/releasenotes/10.2.0.rst | 40 ++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/docs/PIL.rst b/docs/PIL.rst index fa036b9ccfe..493d0bde368 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -69,6 +69,15 @@ can be found here. :undoc-members: :show-inheritance: +:mod:`~PIL.ImageMode` Module +----------------------------- + +.. automodule:: PIL.ImageMode + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: + :mod:`~PIL.ImageTransform` Module --------------------------------- diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 6ab139b560b..f82d20176cc 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -1,14 +1,6 @@ 10.2.0 ------ -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -TODO - Deprecations ============ @@ -20,11 +12,6 @@ ImageFile.raise_oserror error codes returned by a codec's ``decode()`` method, which ImageFile already does automatically. -TODO -^^^^ - -TODO - API Changes =========== @@ -92,6 +79,21 @@ Support has been added to read the BC4U format of DDS images. Support has also been added to read DX10 BC1 and BC4, whether UNORM or TYPELESS. +Optimized ImageColor.getrgb and getcolor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and +:py:attr:`~PIL.ImageColor.getcolor` are now cached using +:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times +as fast and ``getcolor`` are 5.1 - 19.6 times as fast. + +Optimized ImageMode.getmode +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using +:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 20% +faster. + Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -105,3 +107,15 @@ Encoder errors now report error detail as string :py:exc:`OSError` exceptions from image encoders now include a textual description of the error instead of a numeric error code. + +Type hints +^^^^^^^^^^ + +Work has begun to add type annotations to Pillow, including: + +* :py:class:`~PIL.ContainerIO` +* :py:class:`~PIL.FontFile` and subclasses +* :py:class:`~PIL.ImageChops` +* :py:class:`~PIL.ImageMode` +* :py:class:`~PIL.ImageSequence` +* :py:class:`~PIL.TarIO` From 129a4936d33f775e2122b5f21d49ceeeb601e0b5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 31 Dec 2023 09:53:59 -0700 Subject: [PATCH 054/131] Update docs/releasenotes/10.2.0.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- docs/releasenotes/10.2.0.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index f82d20176cc..c08a85c2c10 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -113,9 +113,9 @@ Type hints Work has begun to add type annotations to Pillow, including: -* :py:class:`~PIL.ContainerIO` -* :py:class:`~PIL.FontFile` and subclasses -* :py:class:`~PIL.ImageChops` -* :py:class:`~PIL.ImageMode` -* :py:class:`~PIL.ImageSequence` -* :py:class:`~PIL.TarIO` +* :py:mod:`~PIL.ContainerIO` +* :py:mod:`~PIL.FontFile` and subclasses +* :py:mod:`~PIL.ImageChops` +* :py:mod:`~PIL.ImageMode` +* :py:mod:`~PIL.ImageSequence` +* :py:mod:`~PIL.TarIO` From b7d64ac177e641085baea239b8b4f164d143db8e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 31 Dec 2023 10:30:22 -0700 Subject: [PATCH 055/131] Don't complain about compatibility code for missing optional dependencies --- .coveragerc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index f18c5ea52b0..70dd4e57f2c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,8 +10,8 @@ exclude_also = def create_lut(): # Don't complain about debug code if DEBUG: - # Don't complain about compatibility code - class TypeGuard: + # Don't complain about compatibility code for missing optional dependencies + except ImportError [run] omit = From d26880cda9af73377233040ae14783ea20b0af51 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 31 Dec 2023 20:06:40 +0200 Subject: [PATCH 056/131] Remove unused create_lut() --- .coveragerc | 2 -- Tests/test_imagemorph.py | 9 --------- 2 files changed, 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index 70dd4e57f2c..46df3f90d27 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,8 +6,6 @@ exclude_also = # Don't complain if non-runnable code isn't run if 0: if __name__ == .__main__.: - # Don't complain about code that shouldn't run - def create_lut(): # Don't complain about debug code if DEBUG: # Don't complain about compatibility code for missing optional dependencies diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index ec55aadf9b3..64a1785ea97 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -57,15 +57,6 @@ def test_str_to_img(): assert_image_equal_tofile(A, "Tests/images/morph_a.png") -def create_lut(): - for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): - lb = ImageMorph.LutBuilder(op_name=op) - lut = lb.build_lut() - with open(f"Tests/images/{op}.lut", "wb") as f: - f.write(lut) - - -# create_lut() @pytest.mark.parametrize( "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") ) From 9e835ca5be86818bdfce583cd4f544f2cb741db5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 13:51:45 +1100 Subject: [PATCH 057/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1dc8e9aaaa6..2aeae2d5ca2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,21 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Concatenate multiple JPEG EXIF markers #7496 + [radarhere] + +- Changed IPTC tile tuple to match other plugins #7661 + [radarhere] + +- Do not assign new fp attribute when exiting context manager #7566 + [radarhere] + +- Support arbitrary masks for uncompressed RGB DDS images #7589 + [radarhere, akx] + +- Support setting ROWSPERSTRIP tag #7654 + [radarhere] + - Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662 [radarhere] From 9bcd4770582ecd0e66479671e271c91b5926a80b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 14:19:16 +1100 Subject: [PATCH 058/131] Use consistent language --- docs/releasenotes/10.2.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index c08a85c2c10..46cfe1599aa 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -91,16 +91,16 @@ Optimized ImageMode.getmode ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using -:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 20% -faster. +:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as +fast. Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and :py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the -histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3x as fast -on average and ``st.extrema`` is 12x as fast on average. +histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on +average and ``st.extrema`` is 12 times as fast on average. Encoder errors now report error detail as string ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From ca48ac0e15075a0a986826f094c2b68aa8f4ac26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 14:31:07 +1100 Subject: [PATCH 059/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 2aeae2d5ca2..6da7b28d49d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Allow uncompressed TIFF images to be saved in chunks #7650 + [radarhere] + - Concatenate multiple JPEG EXIF markers #7496 [radarhere] From 17911d6ec4de6a19142bb49897876684f6f5000f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 14:49:01 +1100 Subject: [PATCH 060/131] Removed import --- src/PIL/IptcImagePlugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 7b6123c667f..6b5e34c4f10 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -172,8 +172,6 @@ def getiptcinfo(im): :returns: A dictionary containing IPTC information, or None if no IPTC information block was found. """ - import io - from . import JpegImagePlugin, TiffImagePlugin data = None @@ -208,7 +206,7 @@ class FakeImage: # parse the IPTC information chunk im.info = {} - im.fp = io.BytesIO(data) + im.fp = BytesIO(data) try: im._open() From 2c75cac4002bffca9691af7c28732ed51cc85929 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 15:25:29 +1100 Subject: [PATCH 061/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6da7b28d49d..6062ad7421e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Deprecate IptcImagePlugin helpers #7664 + [nulano, hugovk, radarhere] + - Allow uncompressed TIFF images to be saved in chunks #7650 [radarhere] From 8422af20d583112156749e3519d3796cf6a1312b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 15:47:53 +1100 Subject: [PATCH 062/131] Removed unnecessary "pragma: no cover" --- src/PIL/ImageFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 021b40c0ef4..035b83c4d77 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -396,7 +396,7 @@ def __init__(self, size, table, channels=3, target_mode=None, **kwargs): if hasattr(table, "shape"): try: import numpy - except ImportError: # pragma: no cover + except ImportError: pass if numpy and isinstance(table, numpy.ndarray): From af026fdd3ce08c4865467fe3343dc8d7360af8da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:06:09 +1100 Subject: [PATCH 063/131] Added decompression bomb check to ImageFont.getmask() --- src/PIL/ImageFont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7f0366ddb5d..78c6b9ecacd 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -150,6 +150,7 @@ def getmask(self, text, mode="", *args, **kwargs): :py:mod:`PIL.Image.core` interface module. """ _string_length_check(text) + Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) def getbbox(self, text, *args, **kwargs): From 8676cbd4e7adafc0b5972ab51fed7b06ab8719d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:13:24 +1100 Subject: [PATCH 064/131] Do not try and crop glyphs from outside of source ImageFont image --- docs/releasenotes/10.2.0.rst | 10 ++++++++++ src/_imaging.c | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 0244b6f7792..4e7c587568e 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -77,6 +77,16 @@ Pillow will now raise a :py:exc:`ValueError` if the number of characters passed 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``. +A decompression bomb check has also been added to +:py:meth:`PIL.ImageFont.ImageFont.getmask`. + +ImageFont.getmask: Trim glyph size +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using PIL fonts, +:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that +they do not extend beyond the bitmap image. + ImageMath.eval: Restricted environment keys ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/_imaging.c b/src/_imaging.c index 2270c77fe7e..e06780c7563 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2649,6 +2649,18 @@ _font_new(PyObject *self_, PyObject *args) { self->glyphs[i].sy0 = S16(B16(glyphdata, 14)); self->glyphs[i].sx1 = S16(B16(glyphdata, 16)); self->glyphs[i].sy1 = S16(B16(glyphdata, 18)); + + // Do not allow glyphs to extend beyond bitmap image + // Helps prevent DOS by stopping cropped images being larger than the original + if (self->glyphs[i].sx1 > self->bitmap->xsize) { + self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize; + self->glyphs[i].sx1 = self->bitmap->xsize; + } + if (self->glyphs[i].sy1 > self->bitmap->ysize) { + self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize; + self->glyphs[i].sy1 = self->bitmap->ysize; + } + if (self->glyphs[i].dy0 < y0) { y0 = self->glyphs[i].dy0; } From ecd3948b45ca9c4c2e8b0b3cb58fe6af9798cfa2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:03:43 +1100 Subject: [PATCH 065/131] Test PILfont even when FreeType is supported --- Tests/test_imagefontpil.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 21b4dee3c4a..fd07ee23b13 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,14 +1,22 @@ from __future__ import annotations +import struct import pytest +from io import BytesIO -from PIL import Image, ImageDraw, ImageFont, features +from PIL import Image, ImageDraw, ImageFont, features, _util from .helper import assert_image_equal_tofile -pytestmark = pytest.mark.skipif( - features.check_module("freetype2"), - reason="PILfont superseded if FreeType is supported", -) +original_core = ImageFont.core + + +def setup_module(): + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) + + +def teardown_module(): + ImageFont.core = original_core def test_default_font(): @@ -44,3 +52,23 @@ def test_textbbox(): default_font = ImageFont.load_default() assert d.textlength("test", font=default_font) == 24 assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) + + +def test_decompression_bomb(): + glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (256, 256))) + with pytest.raises(Image.DecompressionBombError): + font.getmask("A" * 1_000_000) + + +@pytest.mark.timeout(4) +def test_oom(): + glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 32767, 32767, 0, 0, 32767, 32767) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (1, 1))) + font.getmask("A" * 1_000_000) From 6cad0d62e7020e80fe706f7a239a2ec588f6f1b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:14:45 +1100 Subject: [PATCH 066/131] Do not crop again if glyph is the same as the previous one --- src/_imaging.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index e06780c7563..8b5cac180bc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2733,7 +2733,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) { static PyObject * _font_getmask(ImagingFontObject *self, PyObject *args) { Imaging im; - Imaging bitmap; + Imaging bitmap = NULL; int x, b; int i = 0; int status; @@ -2765,10 +2765,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { b = self->baseline; for (x = 0; text[i]; i++) { glyph = &self->glyphs[text[i]]; - bitmap = - ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); - if (!bitmap) { - goto failed; + if (i == 0 || text[i] != text[i - 1]) { + ImagingDelete(bitmap); + bitmap = + ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + if (!bitmap) { + goto failed; + } } status = ImagingPaste( im, @@ -2778,17 +2781,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { glyph->dy0 + b, glyph->dx1 + x, glyph->dy1 + b); - ImagingDelete(bitmap); if (status < 0) { goto failed; } x = x + glyph->dx; b = b + glyph->dy; } + ImagingDelete(bitmap); free(text); return PyImagingNew(im); failed: + ImagingDelete(bitmap); free(text); ImagingDelete(im); Py_RETURN_NONE; From 492e5b0e0aa36e1d2d9a0d71280a9e1449b261dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 16:58:03 +1100 Subject: [PATCH 067/131] Do not set default value for unused variable --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 8b5cac180bc..e0e5f804ad0 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2742,7 +2742,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { PyObject *encoded_string; unsigned char *text; - char *mode = ""; + char *mode; if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) { return NULL; From 1c183827e435b32cd192e6308a5057dbcfb56cea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:47:37 +1100 Subject: [PATCH 068/131] Added release notes for #7589 --- docs/releasenotes/10.2.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 0244b6f7792..ca575073450 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -96,6 +96,12 @@ Support has been added to read the BC4U format of DDS images. Support has also been added to read DX10 BC1 and BC4, whether UNORM or TYPELESS. +Support arbitrary masks for uncompressed RGB DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All masks are now supported when reading DDS images with uncompressed RGB data, +allowing for bit counts other than 24 and 32. + Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From f6aa00dd1eea939a1e0841d4cc7fb7a9e0909de5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 17:00:50 +1100 Subject: [PATCH 069/131] Added release notes for #7654 --- docs/releasenotes/10.2.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index ca575073450..4ed8df98ae0 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -102,6 +102,12 @@ Support arbitrary masks for uncompressed RGB DDS images All masks are now supported when reading DDS images with uncompressed RGB data, allowing for bit counts other than 24 and 32. +Saving TIFF tag RowsPerStrip +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by +the user, rather than always being calculated by Pillow. + Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 2aa08a59eaca9bf839909e5a6760992d27602fe9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 18:20:25 +1100 Subject: [PATCH 070/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6062ad7421e..3c8b4d1153d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Trim glyph size in ImageFont.getmask() #7669 + [radarhere] + - Deprecate IptcImagePlugin helpers #7664 [nulano, hugovk, radarhere] From 681f8183f3e7b0755f345e8cbf12237af51d5492 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 1 Jan 2024 10:01:42 +0200 Subject: [PATCH 071/131] Remove member-order Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/PIL.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/PIL.rst b/docs/PIL.rst index 493d0bde368..b617d920577 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -74,7 +74,6 @@ can be found here. .. automodule:: PIL.ImageMode :members: - :member-order: bysource :undoc-members: :show-inheritance: From c08426b8343d1b84e02068df877e24e294cc9cbd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 1 Jan 2024 10:02:22 +0200 Subject: [PATCH 072/131] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/PIL.rst | 2 +- docs/releasenotes/10.2.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PIL.rst b/docs/PIL.rst index b617d920577..b6944e234a5 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -70,7 +70,7 @@ can be found here. :show-inheritance: :mod:`~PIL.ImageMode` Module ------------------------------ +---------------------------- .. automodule:: PIL.ImageMode :members: diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 46cfe1599aa..75ed461ba11 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -114,7 +114,7 @@ Type hints Work has begun to add type annotations to Pillow, including: * :py:mod:`~PIL.ContainerIO` -* :py:mod:`~PIL.FontFile` and subclasses +* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile` * :py:mod:`~PIL.ImageChops` * :py:mod:`~PIL.ImageMode` * :py:mod:`~PIL.ImageSequence` From 4da1e490368109658cbd1c367cad016b59a7293c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 19:22:01 +1100 Subject: [PATCH 073/131] Added type hints --- src/PIL/Image.py | 2 +- src/PIL/ImageTransform.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 055dbda7a03..065b22dc9cd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2643,7 +2643,7 @@ def transform( resample=Resampling.NEAREST, fill=1, fillcolor=None, - ): + ) -> Image: """ Transforms this image. This method creates a new image with the given size, and the same mode as the original, and copies data diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 1fdaa9140ff..1dc3e210e88 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -14,17 +14,26 @@ # from __future__ import annotations +from typing import Sequence + from . import Image class Transform(Image.ImageTransformHandler): - def __init__(self, data): + method: int + + def __init__(self, data: Sequence[int]) -> None: self.data = data - def getdata(self): + def getdata(self) -> tuple[int, Sequence[int]]: return self.method, self.data - def transform(self, size, image, **options): + def transform( + self, + size: tuple[int, int], + image: Image.Image, + **options: dict[str, str | int | tuple[int, ...] | list[int]], + ) -> Image.Image: # can be overridden method, data = self.getdata() return image.transform(size, method, data, **options) From 59100652b6e9d4eeb82c34d446fea0a1736a5942 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 20:33:34 +1100 Subject: [PATCH 074/131] Added type hints to ImageTransform --- docs/releasenotes/10.2.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index f8ef4b8c85a..d1f549e9c51 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -149,4 +149,5 @@ Work has begun to add type annotations to Pillow, including: * :py:mod:`~PIL.ImageChops` * :py:mod:`~PIL.ImageMode` * :py:mod:`~PIL.ImageSequence` +* :py:mod:`~PIL.ImageTransform` * :py:mod:`~PIL.TarIO` From 09ea12107940d6831c300db3bd6ed29a0888c5b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:09:01 +1100 Subject: [PATCH 075/131] Use enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/PIL/ImageTransform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 1dc3e210e88..84c81f1844f 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -20,7 +20,7 @@ class Transform(Image.ImageTransformHandler): - method: int + method: Image.Transform def __init__(self, data: Sequence[int]) -> None: self.data = data From 0eb661b88971b75738cb7fe03b77e57158188e70 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 11:35:42 +0100 Subject: [PATCH 076/131] do not crop ImageFont glyphs from negative coordinates --- Tests/test_imagefontpil.py | 4 +++- src/_imaging.c | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index fd07ee23b13..9e085510149 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -66,7 +66,9 @@ def test_decompression_bomb(): @pytest.mark.timeout(4) def test_oom(): - glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 32767, 32767, 0, 0, 32767, 32767) + glyph = struct.pack( + ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 + ) fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) font = ImageFont.ImageFont() diff --git a/src/_imaging.c b/src/_imaging.c index e0e5f804ad0..59f80a35415 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2652,6 +2652,14 @@ _font_new(PyObject *self_, PyObject *args) { // Do not allow glyphs to extend beyond bitmap image // Helps prevent DOS by stopping cropped images being larger than the original + if (self->glyphs[i].sx0 < 0) { + self->glyphs[i].dx0 -= self->glyphs[i].sx0; + self->glyphs[i].sx0 = 0; + } + if (self->glyphs[i].sy0 < 0) { + self->glyphs[i].dy0 -= self->glyphs[i].sy0; + self->glyphs[i].sy0 = 0; + } if (self->glyphs[i].sx1 > self->bitmap->xsize) { self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize; self->glyphs[i].sx1 = self->bitmap->xsize; From aed764fe8404926472499208a39e5bf90d861b2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 23:39:04 +1100 Subject: [PATCH 077/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3c8b4d1153d..006b6d5673d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,8 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- -- Trim glyph size in ImageFont.getmask() #7669 - [radarhere] +- Trim glyph size in ImageFont.getmask() #7669, #7672 + [radarhere, nulano] - Deprecate IptcImagePlugin helpers #7664 [nulano, hugovk, radarhere] From 7c526a6c6bdc7cb947f0aee1d1ee17c266ff6c61 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 23:59:13 +1100 Subject: [PATCH 078/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 006b6d5673d..1e872d1729d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553 + [bgilbert, radarhere] + - Trim glyph size in ImageFont.getmask() #7669, #7672 [radarhere, nulano] From de62b25ed318f1604aa4ccd6f942a04c6b2c8b59 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 18:06:46 +0100 Subject: [PATCH 079/131] fix image url in "Reading from URL" example --- docs/handbook/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index d79f2465f16..523e2ad7494 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -542,7 +542,7 @@ Reading from URL from PIL import Image from urllib.request import urlopen - url = "https://python-pillow.org/images/pillow-logo.png" + url = "https://python-pillow.org/assets/images/pillow-logo.png" img = Image.open(urlopen(url)) From cb41b0cc78eeefbd9ed2ce8c10f8d6d4c405a706 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 17:16:45 +0000 Subject: [PATCH 080/131] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.7 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.7...v0.1.9) - [github.com/psf/black-pre-commit-mirror: 23.12.0 → 23.12.1](https://github.com/psf/black-pre-commit-mirror/compare/23.12.0...23.12.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1c4b801527..6adc75b4902 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 + rev: v0.1.9 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black From 90991428fa4ca39076efa6329792f30b962d7d49 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 20:05:16 +0100 Subject: [PATCH 081/131] add LCMS2 flags to ImageCms --- Tests/test_imagecms.py | 10 +++++++ src/PIL/ImageCms.py | 63 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 0dde82bd748..fec482f4393 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -90,6 +90,16 @@ def test_sanity(): hopper().point(t) +def test_flags(): + assert ImageCms.Flags.NONE == 0 + assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE + assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE + + assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16) + assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255) + assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) + + def test_name(): skip_missing() # get profile information for file diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 643fce830ba..eafafd58333 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -16,8 +16,10 @@ # below for the original description. from __future__ import annotations +import operator import sys -from enum import IntEnum +from enum import IntEnum, IntFlag +from functools import reduce from . import Image @@ -119,6 +121,48 @@ class Direction(IntEnum): # # flags + +class Flags(IntFlag): + # These are taken from lcms2.h (including comments) + NONE = 0 + NOCACHE = 0x0040 # Inhibit 1-pixel cache + NOOPTIMIZE = 0x0100 # Inhibit optimizations + NULLTRANSFORM = 0x0200 # Don't transform anyway + GAMUTCHECK = 0x1000 # Out of Gamut alarm + SOFTPROOFING = 0x4000 # Do softproofing + BLACKPOINTCOMPENSATION = 0x2000 + NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot + HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy + LOWRESPRECALC = 0x0800 # Use less memory to minimize resources + # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: + USE_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks + GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink) + KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation + FORCE_CLUT = 0x0002 # Force CLUT optimization + CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible + CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible + NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms + COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform() + NODEFAULTRESOURCEDEF = 0x01000000 + + _GRIDPOINTS_1 = 1 << 16 + _GRIDPOINTS_2 = 2 << 16 + _GRIDPOINTS_4 = 4 << 16 + _GRIDPOINTS_8 = 8 << 16 + _GRIDPOINTS_16 = 16 << 16 + _GRIDPOINTS_32 = 32 << 16 + _GRIDPOINTS_64 = 64 << 16 + _GRIDPOINTS_128 = 128 << 16 + + @staticmethod + def GRIDPOINTS(n: int) -> Flags: + # Fine-tune control over number of gridpoints + return Flags.NONE | ((n & 0xFF) << 16) + + +_MAX_FLAG = reduce(operator.or_, Flags) + + FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, @@ -142,11 +186,6 @@ class Direction(IntEnum): "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints } -_MAX_FLAG = 0 -for flag in FLAGS.values(): - if isinstance(flag, int): - _MAX_FLAG = _MAX_FLAG | flag - # --------------------------------------------------------------------. # Experimental PIL-level API @@ -218,7 +257,7 @@ def __init__( intent=Intent.PERCEPTUAL, proof=None, proof_intent=Intent.ABSOLUTE_COLORIMETRIC, - flags=0, + flags=Flags.NONE, ): if proof is None: self.transform = core.buildTransform( @@ -303,7 +342,7 @@ def profileToProfile( renderingIntent=Intent.PERCEPTUAL, outputMode=None, inPlace=False, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Applies an ICC transformation to a given image, mapping from @@ -420,7 +459,7 @@ def buildTransform( inMode, outMode, renderingIntent=Intent.PERCEPTUAL, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -482,7 +521,7 @@ def buildTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: @@ -505,7 +544,7 @@ def buildProofTransform( outMode, renderingIntent=Intent.PERCEPTUAL, proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC, - flags=FLAGS["SOFTPROOFING"], + flags=Flags.SOFTPROOFING, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -586,7 +625,7 @@ def buildProofTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: From 26b2aa5165c62ee38664e291206f8ff28a30bb39 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 02:07:59 +0100 Subject: [PATCH 082/131] document ImageCms.{ImageCmsProfile,Intent,Direction}; fix ImageCms.core.CmsProfile references (cherry picked from commit f2b1bbcf65b327c14646d4113302e3df59555110) --- docs/deprecations.rst | 4 ++-- docs/reference/ImageCms.rst | 21 ++++++++++++++++++++- docs/releasenotes/8.0.0.rst | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a42dc555fea..0f9c75756ee 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -338,8 +338,8 @@ ImageCms.CmsProfile attributes .. deprecated:: 3.2.0 .. versionremoved:: 8.0.0 -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, -they issued a :py:exc:`DeprecationWarning`: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed. +From 6.0.0, they issued a :py:exc:`DeprecationWarning`: ======================== =================================================== Removed Use instead diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b9b5e7b29e..9b6be40b106 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -8,9 +8,26 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management support using the LittleCMS2 color management engine, based on Kevin Cazabon's PyCMS library. +.. autoclass:: ImageCmsProfile + :members: + :special-members: __init__ .. autoclass:: ImageCmsTransform + :members: + :undoc-members: .. autoexception:: PyCMSError +Constants +--------- + +.. autoclass:: Intent + :members: + :member-order: bysource + :undoc-members: +.. autoclass:: Direction + :members: + :member-order: bysource + :undoc-members: + Functions --------- @@ -37,13 +54,15 @@ CmsProfile ---------- The ICC color profiles are wrapped in an instance of the class -:py:class:`CmsProfile`. The specification ICC.1:2010 contains more +:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more information about the meaning of the values in ICC profiles. For convenience, all XYZ-values are also given as xyY-values (so they can be easily displayed in a chromaticity diagram, for example). +.. py:currentmodule:: PIL.ImageCms.core .. py:class:: CmsProfile + :canonical: PIL._imagingcms.CmsProfile .. py:attribute:: creation_date :type: Optional[datetime.datetime] diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 2bf299dd3d8..1fc245c9a3c 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -30,7 +30,7 @@ Image.fromstring, im.fromstring and im.tostring ImageCms.CmsProfile attributes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed: ======================== =================================================== Removed Use instead From 0b2e2b224fe430c74995664c0d8eec9df789727e Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 21:38:29 +0100 Subject: [PATCH 083/131] document ImageCms.Flags --- docs/reference/ImageCms.rst | 4 +++ src/PIL/ImageCms.py | 57 +++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b6be40b106..4ef5ac77456 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -27,6 +27,10 @@ Constants :members: :member-order: bysource :undoc-members: +.. autoclass:: Flags + :members: + :member-order: bysource + :undoc-members: Functions --------- diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index eafafd58333..9a7afe81f2b 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -123,26 +123,43 @@ class Direction(IntEnum): class Flags(IntFlag): - # These are taken from lcms2.h (including comments) + """Flags and documentation are taken from ``lcms2.h``.""" + NONE = 0 - NOCACHE = 0x0040 # Inhibit 1-pixel cache - NOOPTIMIZE = 0x0100 # Inhibit optimizations - NULLTRANSFORM = 0x0200 # Don't transform anyway - GAMUTCHECK = 0x1000 # Out of Gamut alarm - SOFTPROOFING = 0x4000 # Do softproofing + NOCACHE = 0x0040 + """Inhibit 1-pixel cache""" + NOOPTIMIZE = 0x0100 + """Inhibit optimizations""" + NULLTRANSFORM = 0x0200 + """Don't transform anyway""" + GAMUTCHECK = 0x1000 + """Out of Gamut alarm""" + SOFTPROOFING = 0x4000 + """Do softproofing""" BLACKPOINTCOMPENSATION = 0x2000 - NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot - HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy - LOWRESPRECALC = 0x0800 # Use less memory to minimize resources + NOWHITEONWHITEFIXUP = 0x0004 + """Don't fix scum dot""" + HIGHRESPRECALC = 0x0400 + """Use more memory to give better accuracy""" + LOWRESPRECALC = 0x0800 + """Use less memory to minimize resources""" # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: - USE_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks - GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink) - KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation - FORCE_CLUT = 0x0002 # Force CLUT optimization - CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible - CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible - NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms - COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform() + USE_8BITS_DEVICELINK = 0x0008 + """Create 8 bits devicelinks""" + GUESSDEVICECLASS = 0x0020 + """Guess device class (for transform2devicelink)""" + KEEP_SEQUENCE = 0x0080 + """Keep profile sequence for devicelink creation""" + FORCE_CLUT = 0x0002 + """Force CLUT optimization""" + CLUT_POST_LINEARIZATION = 0x0001 + """create postlinearization tables if possible""" + CLUT_PRE_LINEARIZATION = 0x0010 + """create prelinearization tables if possible""" + NONEGATIVES = 0x8000 + """Prevent negative numbers in floating point transforms""" + COPY_ALPHA = 0x04000000 + """Alpha channels are copied on cmsDoTransform()""" NODEFAULTRESOURCEDEF = 0x01000000 _GRIDPOINTS_1 = 1 << 16 @@ -156,7 +173,11 @@ class Flags(IntFlag): @staticmethod def GRIDPOINTS(n: int) -> Flags: - # Fine-tune control over number of gridpoints + """ + Fine-tune control over number of gridpoints + + :param n: :py:class:`int` in range ``0 <= n <= 255`` + """ return Flags.NONE | ((n & 0xFF) << 16) From 6956d0b2853f5c7ec5f6ec4c60725c5a7ee73aeb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 18:32:31 +1100 Subject: [PATCH 084/131] 10.2.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 1e872d1729d..85036f6425b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changelog (Pillow) ================== -10.2.0 (unreleased) +10.2.0 (2024-01-02) ------------------- - Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 7d994caf4d7..1018b96b525 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.2.0.dev0" +__version__ = "10.2.0" From fd148246499bfd5c7d55f871e3f1f6e608a9bc3f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 19:15:13 +1100 Subject: [PATCH 085/131] bbox on macOS is not 2x on retina screens --- docs/reference/ImageGrab.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 0b94032d5f8..5c365132f5e 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -22,7 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. - Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. + On macOS, this is not increased to 2x for retina screens, so the full + width of a retina screen would be 1440, not 2880. + On Windows OS, the top-left point may be negative if + ``all_screens=True`` is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 From dacd92853094414ab6a4326e7f5805666b92996c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:37:59 +1100 Subject: [PATCH 086/131] 10.3.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 1018b96b525..0568943b52b 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.2.0" +__version__ = "10.3.0.dev0" From ec6a57f69d45c0444d7e38df57af6761524463fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 21:27:12 +1100 Subject: [PATCH 087/131] Updated description Co-authored-by: Hugo van Kemenade --- docs/reference/ImageGrab.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 5c365132f5e..e0e8d5a2f1b 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -22,10 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. - On macOS, this is not increased to 2x for retina screens, so the full - width of a retina screen would be 1440, not 2880. - On Windows OS, the top-left point may be negative if - ``all_screens=True`` is used. + On macOS, this is not increased to 2x for Retina screens, so the full + width of a Retina screen would be 1440, not 2880. + On Windows, the top-left point may be negative if ``all_screens=True`` + is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 From 75015e9859183e426012aa59309473c479b59a59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 22:43:56 +1100 Subject: [PATCH 088/131] Skip PyPy3.8 Windows wheel --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 060fc497ea7..5adff7ec1c4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -143,6 +143,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" + CIBW_SKIP: pp38-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow From 5ddcf4d11493524627dbf4b65ad0ed76b7625168 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:33:48 +1100 Subject: [PATCH 089/131] Package name is now lowercase in wheel filenames --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index b3fd72a520e..97f4f8dcd18 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -87,7 +87,7 @@ Released as needed privately to individual vendors for critical security-related and copy into `dist`. Check and upload them e.g.: ```bash python3 -m twine check --strict dist/* - python3 -m twine upload dist/Pillow-5.2.0* + python3 -m twine upload dist/pillow-5.2.0* ``` ## Publicize Release From 81ea98e4941af0940b4725a7cc187c689a3726e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Tue, 2 Jan 2024 14:10:07 +0100 Subject: [PATCH 090/131] document ImageCmsTransform's base class Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/reference/ImageCms.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 4ef5ac77456..22ed516ce58 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -14,6 +14,7 @@ Cazabon's PyCMS library. .. autoclass:: ImageCmsTransform :members: :undoc-members: + :show-inheritance: .. autoexception:: PyCMSError Constants From fc7088a561554aec8b6a4cf6517fdcc11554cb6b Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 14:52:12 +0100 Subject: [PATCH 091/131] improve ImageTransform documentation --- docs/PIL.rst | 8 ------- docs/reference/ImageTransform.rst | 35 +++++++++++++++++++++++++++++++ docs/reference/index.rst | 1 + src/PIL/Image.py | 4 ++++ src/PIL/ImageTransform.py | 13 +++++++----- 5 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 docs/reference/ImageTransform.rst diff --git a/docs/PIL.rst b/docs/PIL.rst index b6944e234a5..bdbf1373d88 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -77,14 +77,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageTransform` Module ---------------------------------- - -.. automodule:: PIL.ImageTransform - :members: - :undoc-members: - :show-inheritance: - :mod:`~PIL.PaletteFile` Module ------------------------------ diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst new file mode 100644 index 00000000000..1278801821d --- /dev/null +++ b/docs/reference/ImageTransform.rst @@ -0,0 +1,35 @@ + +.. py:module:: PIL.ImageTransform +.. py:currentmodule:: PIL.ImageTransform + +:py:mod:`~PIL.ImageTransform` Module +==================================== + +The :py:mod:`~PIL.ImageTransform` module contains implementations of +:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin +:py:class:`.Image.Transform` methods. + +.. autoclass:: Transform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: AffineTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: ExtentTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: QuadTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MeshTransform + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 5d6affa94ad..82c75e373ad 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -25,6 +25,7 @@ Reference ImageShow ImageStat ImageTk + ImageTransform ImageWin ExifTags TiffTags diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1bba9aad2c1..c56da545803 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2666,6 +2666,10 @@ class Example(Image.ImageTransformHandler): def transform(self, size, data, resample, fill=1): # Return result + Implementations of :py:class:`~PIL.Image.ImageTransformHandler` + for some of the :py:class:`Transform` methods are provided + in :py:mod:`~PIL.ImageTransform`. + It may also be an object with a ``method.getdata`` method that returns a tuple supplying new ``method`` and ``data`` values:: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 84c81f1844f..4f79500e64e 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -20,12 +20,14 @@ class Transform(Image.ImageTransformHandler): + """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" + method: Image.Transform def __init__(self, data: Sequence[int]) -> None: self.data = data - def getdata(self) -> tuple[int, Sequence[int]]: + def getdata(self) -> tuple[Image.Transform, Sequence[int]]: return self.method, self.data def transform( @@ -34,6 +36,7 @@ def transform( image: Image.Image, **options: dict[str, str | int | tuple[int, ...] | list[int]], ) -> Image.Image: + """Perform the transform. Called from :py:meth:`.Image.transform`.""" # can be overridden method, data = self.getdata() return image.transform(size, method, data, **options) @@ -51,7 +54,7 @@ class AffineTransform(Transform): This function can be used to scale, translate, rotate, and shear the original image. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows from an affine transform matrix. @@ -73,7 +76,7 @@ class ExtentTransform(Transform): rectangle in the current image. It is slightly slower than crop, but about as fast as a corresponding resize operation. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the input image's coordinate system. See :ref:`coordinate-system`. @@ -89,7 +92,7 @@ class QuadTransform(Transform): Maps a quadrilateral (a region defined by four corners) from the image to a rectangle of the given size. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the upper left, lower left, lower right, and upper right corner of the @@ -104,7 +107,7 @@ class MeshTransform(Transform): Define a mesh image transform. A mesh transform consists of one or more individual quad transforms. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param data: A list of (bbox, quad) tuples. """ From b4a82712887e14b7ff1fc6302fdd9b1a48ac2280 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 17:26:11 +0100 Subject: [PATCH 092/131] update Windows 11 tested versions --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index fbcfbb90724..4c58b4ebb27 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -581,9 +581,9 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+----------------------------+------------------+--------------+ | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm64 | +| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 Pro | 3.11, 3.12 | 10.1.0 |x86-64 | +| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ | Windows 10 | 3.7 | 7.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ From d134110ace31194f5fab4a9c3a62563a5d2a91bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jan 2024 09:01:35 +1100 Subject: [PATCH 093/131] If bbox is omitted, screenshot is taken at 2x on Retina screens --- docs/reference/ImageGrab.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index e0e8d5a2f1b..db2987eb081 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,9 +11,9 @@ or the clipboard to a PIL image memory. .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) - Take a snapshot of the screen. The pixels inside the bounding box are - returned as an "RGBA" on macOS, or an "RGB" image otherwise. - If the bounding box is omitted, the entire screen is copied. + Take a snapshot of the screen. The pixels inside the bounding box are returned as + an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, + the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. 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 From 424737ef4906b5005f53b1642e5b538cadf391ed Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jan 2024 11:18:16 +1100 Subject: [PATCH 094/131] Updated macOS tested Pillow versions --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 4c58b4ebb27..922720b9d05 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -510,7 +510,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm | +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | | +----------------------------+------------------+ | From 05e73702f2651897efbdc0ad39551f1a99b371d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 10:51:10 +1100 Subject: [PATCH 095/131] Updated matrix variable name on Linux and macOS to match Windows --- .github/workflows/wheels.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5adff7ec1c4..85d9eba1c3e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -39,18 +39,18 @@ jobs: include: - name: "macOS x86_64" os: macos-latest - archs: x86_64 + cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" os: macos-latest - archs: arm64 + cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 - name: "manylinux_2_28 x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" steps: @@ -67,7 +67,7 @@ jobs: python3 -m pip install -r .ci/requirements-cibw.txt python3 -m cibuildwheel --output-dir wheelhouse env: - CIBW_ARCHS: ${{ matrix.archs }} + CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} @@ -77,7 +77,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}-${{ matrix.archs }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl windows: From 85c552934af5ed378b034560ace23107bb4ec497 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 3 Jan 2024 17:45:41 +0200 Subject: [PATCH 096/131] Goodbye Travis CI --- .github/workflows/test-cygwin.yml | 2 -- .github/workflows/test-docker.yml | 2 -- .github/workflows/test-mingw.yml | 2 -- .github/workflows/test-windows.yml | 2 -- .github/workflows/test.yml | 2 -- .travis.yml | 52 ------------------------------ README.md | 3 -- RELEASING.md | 8 +---- docs/about.rst | 3 +- docs/index.rst | 4 --- 10 files changed, 2 insertions(+), 78 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 32ac6f65e76..7244315ac15 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index eb27b4bf75b..3bb6856f6e8 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 115c2e9bebc..cdd51e2bb3f 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa1e..a0ef1c3f1ec 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -6,7 +6,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -14,7 +13,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7e112f4364..05f78704bcd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8f8250809d7..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -if: tag IS present OR type = api - -env: - global: - - CIBW_ARCHS=aarch64 - - CIBW_SKIP=pp38-* - -language: python -# Default Python version is usually 3.6 -python: "3.12" -dist: jammy -services: docker - -jobs: - include: - - name: "manylinux2014 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014 - - name: "manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28 - - name: "musllinux aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*musllinux*" - -install: - - python3 -m pip install -r .ci/requirements-cibw.txt - -script: - - python3 -m cibuildwheel --output-dir wheelhouse - - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/" - -# Upload wheels to GitHub Releases -deploy: - provider: releases - api_key: $GITHUB_RELEASE_TOKEN - file_glob: true - file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl" - on: - repo: python-pillow/Pillow - tags: true - skip_cleanup: true diff --git a/README.md b/README.md index e11bd2faa1d..6982676f518 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,6 @@ As of 2019, Pillow development is GitHub Actions build status (Wheels) - Travis CI wheels build status (aarch64) Code coverage diff --git a/RELEASING.md b/RELEASING.md index 97f4f8dcd18..62f3627de19 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] 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 pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them. +* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] 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. @@ -83,12 +83,6 @@ Released as needed privately to individual vendors for critical security-related * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. -* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) - and copy into `dist`. Check and upload them e.g.: - ```bash - python3 -m twine check --strict dist/* - python3 -m twine upload dist/pillow-5.2.0* - ``` ## Publicize Release diff --git a/docs/about.rst b/docs/about.rst index 872ac0ea690..da351ce2c56 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,13 +6,12 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `GitHub Actions`_, `AppVeyor`_ and `Travis CI`_ +- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow -.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.org/project/Pillow/ diff --git a/docs/index.rst b/docs/index.rst index 4f577fe9c22..053d55c3c39 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Tue, 2 Jan 2024 18:11:52 +0200 Subject: [PATCH 097/131] Build QEMU-emulated Linux aarch64 wheels on GitHub Actions --- .github/workflows/wheels.yml | 77 +++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85d9eba1c3e..6ebf9a4c029 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -30,7 +30,80 @@ env: FORCE_COLOR: 1 jobs: - build: + build-1-QEMU-emulated-wheels: + name: QEMU ${{ matrix.python-version }} ${{ matrix.spec }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - pp39 + - pp310 + - cp38 + - cp39 + - cp310 + - cp311 + - cp312 + spec: + - manylinux2014 + - manylinux_2_28 + - musllinux + exclude: + - { python-version: pp39, spec: musllinux } + - { python-version: pp310, spec: musllinux } + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install cibuildwheel + run: | + python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels (manylinux) + if: matrix.spec != 'musllinux' + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS_LINUX: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" + # Extra options for manylinux. + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} + + - name: Build wheels (musllinux) + if: matrix.spec == 'musllinux' + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS_LINUX: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" + + - uses: actions/upload-artifact@v4 + with: + name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} + path: ./wheelhouse/*.whl + + build-2-native-wheels: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: @@ -187,7 +260,7 @@ jobs: pypi-publish: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build, windows, sdist] + needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] runs-on: ubuntu-latest name: Upload release to PyPI environment: From 55944860a5dd4a38d786df035346b9d5ddf3aa1e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:50:06 +0200 Subject: [PATCH 098/131] Remove unused docker/setup-buildx-action --- .github/workflows/wheels.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6ebf9a4c029..4de599b810b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -65,10 +65,6 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt From 32ae1bd08a6e3d8b9d266a2e7c8b265ec3d5ad18 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:52:22 +0200 Subject: [PATCH 099/131] Use aarch64 instead of QEMU in job name --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4de599b810b..d7a93c70ce6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -31,7 +31,7 @@ env: jobs: build-1-QEMU-emulated-wheels: - name: QEMU ${{ matrix.python-version }} ${{ matrix.spec }} + name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} runs-on: ubuntu-latest strategy: fail-fast: false From fd37d86accff55d366950b3f9d286c7174ebe9b4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:55:04 +0200 Subject: [PATCH 100/131] Skip non-wheel CI runs for tags --- .github/workflows/test-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa1e..94c2d4d70b8 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -2,6 +2,8 @@ name: Test Windows on: push: + branches: + - "**" paths-ignore: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" From bc3cf97649d76bc231ec99d41dfd6eb2be72b175 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 11:00:11 +1100 Subject: [PATCH 101/131] Use general arch setting instead of platform-specific setting --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d7a93c70ce6..8a9f81dfd09 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -76,7 +76,7 @@ jobs: env: # Build only the currently selected Linux architecture (so we can # parallelise for speed). - CIBW_ARCHS_LINUX: "aarch64" + CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" # Extra options for manylinux. @@ -90,7 +90,7 @@ jobs: env: # Build only the currently selected Linux architecture (so we can # parallelise for speed). - CIBW_ARCHS_LINUX: "aarch64" + CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" From e84b0a401509325c7e5e553a9f1a4591256b4b45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 11:38:41 +1100 Subject: [PATCH 102/131] Combine build steps --- .github/workflows/wheels.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8a9f81dfd09..e2bc78b983c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -69,8 +69,7 @@ jobs: run: | python3 -m pip install -r .ci/requirements-cibw.txt - - name: Build wheels (manylinux) - if: matrix.spec != 'musllinux' + - name: Build wheels run: | python3 -m cibuildwheel --output-dir wheelhouse env: @@ -78,22 +77,11 @@ jobs: # parallelise for speed). CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" # Extra options for manylinux. CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} - - name: Build wheels (musllinux) - if: matrix.spec == 'musllinux' - run: | - python3 -m cibuildwheel --output-dir wheelhouse - env: - # Build only the currently selected Linux architecture (so we can - # parallelise for speed). - CIBW_ARCHS: "aarch64" - # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" - - uses: actions/upload-artifact@v4 with: name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} From 60e82e5a3f21b25e1b54ebc1104e972290201a85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 17:24:09 +1100 Subject: [PATCH 103/131] Separate cibuildwheel install --- .github/workflows/wheels.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e2bc78b983c..50e47f19853 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -119,9 +119,11 @@ jobs: with: python-version: "3.x" - - name: Build wheels + - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels python3 -m cibuildwheel --output-dir wheelhouse env: CIBW_ARCHS: ${{ matrix.cibw_arch }} @@ -163,6 +165,10 @@ jobs: with: python-version: "3.x" + - name: Install cibuildwheel + run: | + & python.exe -m pip install -r .ci/requirements-cibw.txt + - name: Prepare for build run: | choco install nasm --no-progress @@ -171,8 +177,6 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe -m pip install -r .ci/requirements-cibw.txt - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} shell: pwsh From 46db79abe1b8ada6f980e01cee633f9c8f6fbbdf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 00:30:46 -0700 Subject: [PATCH 104/131] Fix syntax --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 50e47f19853..77c1489dfcd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -124,6 +124,7 @@ jobs: python3 -m pip install -r .ci/requirements-cibw.txt - name: Build wheels + run: | python3 -m cibuildwheel --output-dir wheelhouse env: CIBW_ARCHS: ${{ matrix.cibw_arch }} From f184775cd3a2410ad326572d2b1d9a75ac481790 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 4 Jan 2024 23:32:54 +1100 Subject: [PATCH 105/131] Removed leading ampersand Co-authored-by: Hugo van Kemenade --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 77c1489dfcd..be8f652447c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -168,7 +168,7 @@ jobs: - name: Install cibuildwheel run: | - & python.exe -m pip install -r .ci/requirements-cibw.txt + python.exe -m pip install -r .ci/requirements-cibw.txt - name: Prepare for build run: | From 2dd00de1f36d0e5dfd15f28048c2e2b7550bb232 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 20:26:14 +0100 Subject: [PATCH 106/131] rename x64 to AMD64 in winbuild/build_prepare.py --- .appveyor.yml | 2 +- .github/workflows/wheels.yml | 17 +++++++---------- winbuild/build.rst | 6 +++--- winbuild/build_prepare.py | 4 ++-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 0f5dea9c515..4c5a7f9ee47 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,7 +14,7 @@ environment: ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 - ARCHITECTURE: x64 + ARCHITECTURE: AMD64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85d9eba1c3e..36e98aa554a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,18 +81,15 @@ jobs: path: ./wheelhouse/*.whl windows: - name: Windows ${{ matrix.arch }} + name: Windows ${{ matrix.cibw_arch }} runs-on: windows-latest strategy: fail-fast: false matrix: include: - - arch: x86 - cibw_arch: x86 - - arch: x64 - cibw_arch: AMD64 - - arch: ARM64 - cibw_arch: ARM64 + - cibw_arch: x86 + - cibw_arch: AMD64 + - cibw_arch: ARM64 steps: - uses: actions/checkout@v4 @@ -116,7 +113,7 @@ jobs: & python.exe -m pip install -r .ci/requirements-cibw.txt - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh - name: Build wheels @@ -157,13 +154,13 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: dist-windows-${{ matrix.arch }} + name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll uses: actions/upload-artifact@v4 with: - name: fribidi-windows-${{ matrix.arch }} + name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* sdist: diff --git a/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6cc..cd3b559e7f0 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) `_ +* x86/AMD64: `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}] [--nmake] + [--architecture {x86,AMD64,ARM64}] [--nmake] [--no-imagequant] [--no-fribidi] Download and generate build scripts for Pillow dependencies. @@ -55,7 +55,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 {x86,AMD64,ARM64} build architecture (default: same as host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 8e3757ca89e..440e64d9851 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,7 +105,7 @@ def cmd_msbuild( ARCHITECTURES = { "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, - "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, + "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -651,7 +651,7 @@ def build_dep_all() -> None: ( "ARM64" if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64") + else ("x86" if struct.calcsize("P") == 4 else "AMD64") ), ), help="build architecture (default: same as host Python)", From 5e2ebaface37ff71e1b8e4e9b5708ff3eec3a909 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 21:00:06 +0100 Subject: [PATCH 107/131] winbuild: build libwebp using cmake --- winbuild/build_prepare.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 440e64d9851..1615abbdb82 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -175,22 +175,15 @@ def cmd_msbuild( "dir": "libwebp-1.3.2", "license": "COPYING", "build": [ - cmd_rmdir(r"output\release-static"), # clean - cmd_nmake( - "Makefile.vc", - "all", - [ - "CFG=release-static", - "RTLIBCFG=dynamic", - "OBJDIR=output", - "ARCH={architecture}", - "LIBWEBP_BASENAME=webp", - ], + *cmds_cmake( + "webp webpdemux webpmux", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWEBP_LINK_STATIC:BOOL=OFF", ), cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"output\release-static\{architecture}\lib\*.lib"], + "libs": [r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", @@ -204,7 +197,7 @@ def cmd_msbuild( }, r"libtiff\tif_webp.c": { # link against webp.lib - "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 + "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, r"test\CMakeLists.txt": { "add_executable(test_write_read_tags ../placeholder.h)": "", @@ -217,6 +210,7 @@ def cmd_msbuild( *cmds_cmake( "tiff", "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', ) ], From 4094edd12f32a432309d279d7c7b90d068f3bdb1 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 21:26:47 +0100 Subject: [PATCH 108/131] winbuild: fix libwebp linking libsharpyuv --- winbuild/build_prepare.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1615abbdb82..84284ae10f4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -174,6 +174,11 @@ def cmd_msbuild( "filename": "libwebp-1.3.2.tar.gz", "dir": "libwebp-1.3.2", "license": "COPYING", + "patch": { + r"src\enc\picture_csp_enc.c": { + '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 + } + }, "build": [ *cmds_cmake( "webp webpdemux webpmux", @@ -183,7 +188,7 @@ def cmd_msbuild( cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"libwebp*.lib"], + "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", From eff9f06f0dac6521d89938aa5a93ab03a77fd50d Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 22:10:11 +0100 Subject: [PATCH 109/131] fix comments --- winbuild/build_prepare.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 84284ae10f4..df33ea493e6 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -176,6 +176,7 @@ def cmd_msbuild( "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { + # link against libsharpyuv.lib '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 } }, @@ -201,7 +202,7 @@ def cmd_msbuild( "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 }, r"libtiff\tif_webp.c": { - # link against webp.lib + # link against libwebp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, r"test\CMakeLists.txt": { From d329207e62125ee3dcda0b2a82112757e9ef6fbd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Jan 2024 07:03:40 +1100 Subject: [PATCH 110/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 85036f6425b..ac961a680a3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +10.3.0 (unreleased) +------------------- + +- Rename x64 to AMD64 in winbuild #7693 + [nulano] + 10.2.0 (2024-01-02) ------------------- From 2d6ad5868dac031a8b4eeda41116b3da3fd1be58 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Jan 2024 12:07:55 +1100 Subject: [PATCH 111/131] Use "non-zero" consistently --- Tests/test_file_eps.py | 4 ++-- Tests/test_image_resample.py | 2 +- src/libImaging/GifEncode.c | 2 +- src/libImaging/Jpeg.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c479c384a76..8b48e83ad2c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -270,7 +270,7 @@ def test_render_scale1(): image1_scale1_compare.load() assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale1: image2_scale1.load() with Image.open(FILE2_COMPARE) as image2_scale1_compare: @@ -292,7 +292,7 @@ def test_render_scale2(): image1_scale2_compare.load() assert_image_similar(image1_scale2, image1_scale2_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale2: image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index b4bf6c8df3a..5a578dba5c4 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -403,7 +403,7 @@ def test_reduce(self): if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] - def test_nonzero_coefficients(self): + def test_non_zero_coefficients(self): # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index f232454052a..e37301df765 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -105,7 +105,7 @@ static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr, st->head = st->codes[st->probe] >> 20; goto encode_loop; } else { - /* Reprobe decrement must be nonzero and relatively prime to table + /* Reprobe decrement must be non-zero and relatively prime to table * size. So, any odd positive number for power-of-2 size. */ if ((st->probe -= ((st->tail << 2) | 1)) < 0) { st->probe += TABLE_SIZE; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 98eaac28dd6..7cdba902281 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -74,7 +74,7 @@ typedef struct { /* Optimize Huffman tables (slow) */ int optimize; - /* Disable automatic conversion of RGB images to YCbCr if nonzero */ + /* Disable automatic conversion of RGB images to YCbCr if non-zero */ int keep_rgb; /* Stream type (0=full, 1=tables only, 2=image only) */ From 0d841aab9a51a0b9433bc5932dcd913c028301cf Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 6 Jan 2024 13:37:43 +0100 Subject: [PATCH 112/131] add support for grayscale pfm image format --- Tests/images/hopper.pfm | Bin 0 -> 65552 bytes Tests/images/hopper_be.pfm | Bin 0 -> 65551 bytes Tests/test_file_ppm.py | 45 ++++++++++++++++++++++++++- docs/handbook/image-file-formats.rst | 18 +++++++++++ src/PIL/PpmImagePlugin.py | 26 +++++++++++++--- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 Tests/images/hopper.pfm create mode 100644 Tests/images/hopper_be.pfm diff --git a/Tests/images/hopper.pfm b/Tests/images/hopper.pfm new file mode 100644 index 0000000000000000000000000000000000000000..b576615640136f3229ab5a3d29ab7956fd194dd0 GIT binary patch literal 65552 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 L`NZ}X?0@thg2ah6 literal 0 HcmV?d00001 diff --git a/Tests/images/hopper_be.pfm b/Tests/images/hopper_be.pfm new file mode 100644 index 0000000000000000000000000000000000000000..93c75e26fda2cd69362a58f61373b0d88075e51c GIT binary patch literal 65551 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 Date: Sat, 6 Jan 2024 14:33:11 +0100 Subject: [PATCH 113/131] Add code formatting to ImageCms.Flags docstrings Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageCms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 9a7afe81f2b..62b010f4512 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -147,7 +147,7 @@ class Flags(IntFlag): USE_8BITS_DEVICELINK = 0x0008 """Create 8 bits devicelinks""" GUESSDEVICECLASS = 0x0020 - """Guess device class (for transform2devicelink)""" + """Guess device class (for ``transform2devicelink``)""" KEEP_SEQUENCE = 0x0080 """Keep profile sequence for devicelink creation""" FORCE_CLUT = 0x0002 @@ -159,7 +159,7 @@ class Flags(IntFlag): NONEGATIVES = 0x8000 """Prevent negative numbers in floating point transforms""" COPY_ALPHA = 0x04000000 - """Alpha channels are copied on cmsDoTransform()""" + """Alpha channels are copied on ``cmsDoTransform()``""" NODEFAULTRESOURCEDEF = 0x01000000 _GRIDPOINTS_1 = 1 << 16 From a786a0551b75ab3e85da1bfad54226faa2336022 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 16:17:57 +1100 Subject: [PATCH 114/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ac961a680a3..ebf731c5659 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Add LCMS2 flags to ImageCms #7676 + [nulano, radarhere, hugovk] + - Rename x64 to AMD64 in winbuild #7693 [nulano] From bb5527484536f6c8e90ffffd95906c352fecca18 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 18:49:01 +1100 Subject: [PATCH 115/131] Removed PPM loop to read header tokens --- src/PIL/PpmImagePlugin.py | 53 ++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 25dbfa5b0bc..82314214a7f 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -105,40 +105,31 @@ def _open(self): elif magic_number in (b"P3", b"P6"): self.custom_mimetype = "image/x-portable-pixmap" - maxval = None + self._size = int(self._read_token()), int(self._read_token()) + decoder_name = "raw" if magic_number in (b"P1", b"P2", b"P3"): decoder_name = "ppm_plain" - for ix in range(3): - token = int(self._read_token()) - if ix == 0: # token is the x size - xsize = token - elif ix == 1: # token is the y size - ysize = token - if mode == "1": - self._mode = "1" - rawmode = "1;I" - break - else: - self._mode = rawmode = mode - elif ix == 2: # token is maxval - maxval = token - if not 0 < maxval < 65536: - msg = "maxval must be greater than 0 and less than 65536" - raise ValueError(msg) - if maxval > 255 and mode == "L": - self._mode = "I" - - if decoder_name != "ppm_plain": - # If maxval matches a bit depth, use the raw decoder directly - if maxval == 65535 and mode == "L": - rawmode = "I;16B" - elif maxval != 255: - decoder_name = "ppm" - - args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) - self._size = xsize, ysize - self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)] + if mode == "1": + self._mode = "1" + args = "1;I" + else: + maxval = int(self._read_token()) + if not 0 < maxval < 65536: + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) + self._mode = "I" if maxval > 255 and mode == "L" else mode + + rawmode = mode + if decoder_name != "ppm_plain": + # If maxval matches a bit depth, use the raw decoder directly + if maxval == 65535 and mode == "L": + rawmode = "I;16B" + elif maxval != 255: + decoder_name = "ppm" + + args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) + self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] # From ba6399cad14d816cafc44c25782d6a3153082ae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 19:34:27 +1100 Subject: [PATCH 116/131] Added PerspectiveTransform --- Tests/test_image_transform.py | 2 ++ docs/reference/ImageTransform.rst | 5 +++++ src/PIL/ImageTransform.py | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 15939ef647c..578a0a2967a 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -16,6 +16,8 @@ def test_sanity(self): transform = ImageTransform.AffineTransform(seq[:6]) im.transform((100, 100), transform) + transform = ImageTransform.PerspectiveTransform(seq[:8]) + im.transform((100, 100), transform) transform = ImageTransform.ExtentTransform(seq[:4]) im.transform((100, 100), transform) transform = ImageTransform.QuadTransform(seq[:8]) diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst index 1278801821d..5b0a5ce49dc 100644 --- a/docs/reference/ImageTransform.rst +++ b/docs/reference/ImageTransform.rst @@ -19,6 +19,11 @@ The :py:mod:`~PIL.ImageTransform` module contains implementations of :undoc-members: :show-inheritance: +.. autoclass:: PerspectiveTransform + :members: + :undoc-members: + :show-inheritance: + .. autoclass:: ExtentTransform :members: :undoc-members: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 4f79500e64e..6aa82dadd9c 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -63,6 +63,26 @@ class AffineTransform(Transform): method = Image.Transform.AFFINE +class PerspectiveTransform(Transform): + """ + Define a perspective image transform. + + This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel + (x, y) in the output image, the new value is taken from a position + ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in + the input image, rounded to nearest pixel. + + This function can be used to scale, translate, rotate, and shear the + original image. + + See :py:meth:`.Image.transform` + + :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). + """ + + method = Image.Transform.PERSPECTIVE + + class ExtentTransform(Transform): """ Define a transform to extract a subregion from an image. From 08f11c57a131b402f405db76dfd411b441df1ab5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 20:52:47 +0100 Subject: [PATCH 117/131] deprecate ImageCms members: DESCRIPTION, VERSION, FLAGS, versions() --- Tests/test_imagecms.py | 13 +++++++++++-- src/PIL/ImageCms.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index fec482f4393..9575b026db2 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -49,8 +49,8 @@ def skip_missing(): def test_sanity(): # basic smoke test. # this mostly follows the cms_test outline. - - v = ImageCms.versions() # should return four strings + with pytest.warns(DeprecationWarning): + v = ImageCms.versions() # should return four strings assert v[0] == "1.0.0 pil" assert list(map(type, v)) == [str, str, str, str] @@ -637,3 +637,12 @@ def test_rgb_lab(mode): 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_deprecation(): + with pytest.warns(DeprecationWarning): + assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") + with pytest.warns(DeprecationWarning): + assert ImageCms.VERSION == "1.0.0 pil" + with pytest.warns(DeprecationWarning): + assert isinstance(ImageCms.FLAGS, dict) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 62b010f4512..827755bf672 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -20,8 +20,10 @@ import sys from enum import IntEnum, IntFlag from functools import reduce +from typing import Any from . import Image +from ._deprecate import deprecate try: from . import _imagingcms @@ -32,7 +34,7 @@ _imagingcms = DeferredError.new(ex) -DESCRIPTION = """ +_DESCRIPTION = """ pyCMS a Python / PIL interface to the littleCMS ICC Color Management System @@ -95,7 +97,22 @@ """ -VERSION = "1.0.0 pil" +_VERSION = "1.0.0 pil" + + +def __getattr__(name: str) -> Any: + if name == "DESCRIPTION": + deprecate("PIL.ImageCms.DESCRIPTION", 12) + return _DESCRIPTION + elif name == "VERSION": + deprecate("PIL.ImageCms.VERSION", 12) + return _VERSION + elif name == "FLAGS": + deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") + return _FLAGS + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + # --------------------------------------------------------------------. @@ -184,7 +201,7 @@ def GRIDPOINTS(n: int) -> Flags: _MAX_FLAG = reduce(operator.or_, Flags) -FLAGS = { +_FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, "MATRIXONLY": (1 | 2), @@ -1064,4 +1081,9 @@ def versions(): (pyCMS) Fetches versions. """ - return VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ + deprecate( + "PIL.ImageCms.versions()", + 12, + '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', + ) + return _VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ From ccdea48cf379fac585918001f304a7ab58448b96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Jan 2024 10:36:30 +1100 Subject: [PATCH 118/131] Added identity tests for Transform classes --- Tests/test_image_transform.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 578a0a2967a..f5d5ab70408 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -10,20 +10,25 @@ class TestImageTransform: def test_sanity(self): - im = Image.new("L", (100, 100)) - - seq = tuple(range(10)) - - transform = ImageTransform.AffineTransform(seq[:6]) - im.transform((100, 100), transform) - transform = ImageTransform.PerspectiveTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.ExtentTransform(seq[:4]) - im.transform((100, 100), transform) - transform = ImageTransform.QuadTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) - im.transform((100, 100), transform) + im = hopper() + + for transform in ( + ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)), + ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)), + ImageTransform.ExtentTransform((0, 0) + im.size), + ImageTransform.QuadTransform( + (0, 0, 0, im.height, im.width, im.height, im.width, 0) + ), + ImageTransform.MeshTransform( + [ + ( + (0, 0) + im.size, + (0, 0, 0, im.height, im.width, im.height, im.width, 0), + ) + ] + ), + ): + assert_image_equal(im, im.transform(im.size, transform)) def test_info(self): comment = b"File written by Adobe Photoshop\xa8 4.0" From edc46e223b1275f9b197c33011f305af2975afe3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 01:27:41 +0100 Subject: [PATCH 119/131] document ImageCms deprecations --- docs/deprecations.rst | 39 ++++++++++++++++- docs/reference/ImageCms.rst | 3 ++ docs/releasenotes/10.3.0.rst | 83 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/9.1.0.rst | 2 +- docs/releasenotes/index.rst | 1 + 5 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0f9c75756ee..7602f8b4e80 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -55,6 +55,43 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant for internal use, so there is no replacement. They can each be replaced by a single line of code using builtin functions in Python. +ImageCms constants and versions() function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.3.0 + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been +replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``ImageCms.DESCRIPTION`` +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +===================================================== ============================================================ + Removed features ---------------- @@ -118,7 +155,7 @@ Constants .. versionremoved:: 10.0.0 A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 22ed516ce58..c4484cbe22a 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -24,14 +24,17 @@ Constants :members: :member-order: bysource :undoc-members: + :show-inheritance: .. autoclass:: Direction :members: :member-order: bysource :undoc-members: + :show-inheritance: .. autoclass:: Flags :members: :member-order: bysource :undoc-members: + :show-inheritance: Functions --------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 00000000000..ddcb38aa161 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,83 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been replaced +with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``ImageCms.DESCRIPTION`` +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +===================================================== ============================================================ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 02da702a799..6400218f467 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -51,7 +51,7 @@ 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. +(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853cc2..e86f8082b48 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.3.0 10.2.0 10.1.0 10.0.1 From bb855583ea3320b637e7f6511721a9e2655f2b99 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 01:55:32 +0100 Subject: [PATCH 120/131] Update PyPI links to use pillow (lowercase) --- README.md | 4 ++-- docs/about.rst | 2 +- docs/index.rst | 4 ++-- docs/installation.rst | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6982676f518..6ca870166a1 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,10 @@ As of 2019, Pillow development is Tidelift - Newest PyPI version - Number of PyPI downloads `_ and by direct URL access -eg. https://pypi.org/project/Pillow/1.0/. +`_ and by direct URL access +eg. https://pypi.org/project/pillow/1.0/. From bddfebc3315d85bf822192c57a33646d79e738c3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 12:57:23 +0100 Subject: [PATCH 121/131] add license comment to ImageCms; explicitly say "no replacement" for deprecations without a replacement --- docs/deprecations.rst | 4 ++-- docs/releasenotes/10.3.0.rst | 4 ++-- src/PIL/ImageCms.py | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 7602f8b4e80..4c9abe19503 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -67,11 +67,11 @@ replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``ImageCms.DESCRIPTION`` +``ImageCms.DESCRIPTION`` No replacement ``ImageCms.VERSION`` ``PIL.__version__`` ``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` ``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement ``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` ``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` ``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index ddcb38aa161..8dfe34d95fa 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -20,11 +20,11 @@ with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``ImageCms.DESCRIPTION`` +``ImageCms.DESCRIPTION`` No replacement ``ImageCms.VERSION`` ``PIL.__version__`` ``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` ``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement ``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` ``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` ``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 827755bf672..3e40105e46a 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -4,6 +4,9 @@ # Optional color management support, based on Kevin Cazabon's PyCMS # library. +# Originally released under LGPL. Graciously donated to PIL in +# March 2009, for distribution under the standard PIL license + # History: # 2009-03-08 fl Added to PIL. From f044d53fd181446366ab78f570076a665933d4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Mon, 8 Jan 2024 17:17:17 +0100 Subject: [PATCH 122/131] swap conditions Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 6be1278eb2e..d43e21e14db 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,7 @@ def _open(self): for ix in range(3): if mode == "F" and ix == 2: scale = float(self._read_token()) - if not math.isfinite(scale) or scale == 0.0: + if scale == 0.0 or not math.isfinite(scale): msg = "scale must be finite and non-zero" raise ValueError(msg) rawmode = "F;32F" if scale < 0 else "F;32BF" From 5dd1652f2775bb916faa648fe48c7c6350d2c8a4 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 17:16:23 +0100 Subject: [PATCH 123/131] use filename instead of f --- Tests/test_file_ppm.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index efdb880de07..d8e259b1cf8 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -89,20 +89,20 @@ def test_16bit_pgm(): def test_16bit_pgm_write(tmp_path): with Image.open("Tests/images/16_bit_binary.pgm") as im: - f = str(tmp_path / "temp.pgm") - im.save(f, "PPM") + filename = str(tmp_path / "temp.pgm") + im.save(filename, "PPM") - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pnm(tmp_path): with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) - f = str(tmp_path / "temp.pnm") - im.save(f) + filename = str(tmp_path / "temp.pnm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pfm(tmp_path): @@ -110,10 +110,10 @@ def test_pfm(tmp_path): assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) - f = str(tmp_path / "tmp.pfm") - im.save(f) + filename = str(tmp_path / "tmp.pfm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pfm_big_endian(tmp_path): @@ -121,10 +121,10 @@ def test_pfm_big_endian(tmp_path): assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) - f = str(tmp_path / "tmp.pfm") - im.save(f) + filename = str(tmp_path / "tmp.pfm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) @pytest.mark.parametrize( From 586e7740947933454589f3c60b22387e01fa5701 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 17:35:01 +0100 Subject: [PATCH 124/131] add PFM support to release notes --- docs/handbook/image-file-formats.rst | 3 +- docs/releasenotes/10.3.0.rst | 49 ++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 02a6a3af729..569ccb7691b 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -701,7 +701,8 @@ PFM .. versionadded:: 10.3.0 -Pillow reads and writes grayscale (Pf format) PFM files containing ``F`` data. +Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files +containing ``F`` data. Color (PF format) PFM files are not supported. diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 00000000000..34afbe4b844 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,49 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +Portable FloatMap (PFM) images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading and writing grayscale (Pf format) +Portable FloatMap (PFM) files containing ``F`` data. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853cc2..e86f8082b48 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.3.0 10.2.0 10.1.0 10.0.1 From 931821688c0f9ef331097ac8b1358780eb82b21e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:22:25 +1100 Subject: [PATCH 125/131] Added release notes --- docs/releasenotes/10.3.0.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 34afbe4b844..391068769fb 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -26,10 +26,12 @@ TODO API Additions ============= -TODO -^^^^ +Added PerspectiveTransform +^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning +that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding +subclass of :py:class:`~PIL.ImageTransform.Transform`. Security ======== From 6c320323b44df48a5162a2113ae2c34e7c9bf564 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:47:27 +1100 Subject: [PATCH 126/131] Only set row order when needed --- src/PIL/PpmImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 83e028718eb..9d37dcde099 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -311,7 +311,6 @@ def decode(self, buffer): def _save(im, fp, filename): - row_order = 1 if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": @@ -322,7 +321,6 @@ def _save(im, fp, filename): rawmode, head = "RGB", b"P6" elif im.mode == "F": rawmode, head = "F;32F", b"Pf" - row_order = -1 else: msg = f"cannot write mode {im.mode} as PPM" raise OSError(msg) @@ -336,6 +334,7 @@ def _save(im, fp, filename): fp.write(b"65535\n") elif head == b"Pf": fp.write(b"-1.0\n") + row_order = -1 if im.mode == "F" else 1 ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) From ab262dbfd5b8076fd8d530e27c8e4896f03025e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:56:33 +1100 Subject: [PATCH 127/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ebf731c5659..30bbaec3a64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Add support for reading and writing grayscale PFM images #7696 + [nulano, hugovk] + - Add LCMS2 flags to ImageCms #7676 [nulano, radarhere, hugovk] From 1e8a03cd2d938d3773dfdbe9d477276e96527494 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 18:08:40 +1100 Subject: [PATCH 128/131] Link to Python enum documentation --- docs/reference/ExifTags.rst | 5 +++-- docs/releasenotes/10.0.0.rst | 2 +- docs/releasenotes/9.3.0.rst | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 464ab77ea35..06965ead3f0 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,8 +4,9 @@ :py:mod:`~PIL.ExifTags` Module ============================== -The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes -which provide constants and clear-text names for various well-known EXIF tags. +The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` +classes which provide constants and clear-text names for various well-known +EXIF tags. .. py:data:: Base diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index a3f238119f0..705ca04152f 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -43,7 +43,7 @@ Constants ^^^^^^^^^ A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. ===================================================== ============================================================ Removed Use instead diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index fde2faae3a7..16075ce95ec 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -33,8 +33,9 @@ Added ExifTags enums ^^^^^^^^^^^^^^^^^^^^ The data from :py:data:`~PIL.ExifTags.TAGS` and -:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum`` -classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`. +:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as +:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and +:py:data:`~PIL.ExifTags.GPS`. Security From 71ba20bb19899f55761dae1ccd0bc662766cdc65 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 18:22:10 +1100 Subject: [PATCH 129/131] Shortened table description --- docs/deprecations.rst | 54 ++++++++++++++++++------------------ docs/releasenotes/10.3.0.rst | 54 ++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4c9abe19503..205fcb9abcd 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -64,33 +64,33 @@ A number of constants and a function in :py:mod:`.ImageCms` have been deprecated This includes a table of flags based on LittleCMS version 1 which has been replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -===================================================== ============================================================ +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== Removed features ---------------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 8ce6f4b9c9f..548f95df95c 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -17,33 +17,33 @@ A number of constants and a function in :py:mod:`.ImageCms` have been deprecated This includes a table of flags based on LittleCMS version 1 which has been replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -===================================================== ============================================================ +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== API Changes =========== From d7874e8a03dbf57b01c0fe41290603ddfe2875c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 09:07:10 +1100 Subject: [PATCH 130/131] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 30bbaec3a64..c267ca472ce 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Added PerspectiveTransform #7699 + [radarhere] + - Add support for reading and writing grayscale PFM images #7696 [nulano, hugovk] From 5347b471c69c400c6199c8cd200b5844169f7988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 11 Jan 2024 02:08:46 +0100 Subject: [PATCH 131/131] Update Tests/test_imagecms.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_imagecms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 9575b026db2..810394e6f5f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -639,7 +639,7 @@ def test_rgb_lab(mode): assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) -def test_deprecation(): +def test_deprecation() -> None: with pytest.warns(DeprecationWarning): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") with pytest.warns(DeprecationWarning):