From e10a58ccce18c3493e30dfe7222076964311a8d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Oct 2023 21:46:38 +1100 Subject: [PATCH 01/64] 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 02/64] 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 f0861bb4f..98a82cfaa 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1015,6 +1015,11 @@ class TestImage: 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 d435b5617..b6ac1f082 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -528,14 +528,15 @@ class Image: 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 03/64] 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 000000000..8e2866afe --- /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 33dc561e5..9e7d4ed62 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 04/64] 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 b6ac1f082..7e0e14f26 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -527,15 +527,18 @@ class Image: 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 @@ class Image: """ 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 05/64] 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 06/64] 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 65d9deaa0..731c85a7e 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -154,9 +154,7 @@ class DdsImageFile(ImageFile.ImageFile): 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 07/64] 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 731c85a7e..17b1a6082 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -258,10 +258,9 @@ class DdsRgbDecoder(ImageFile.PyDecoder): 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 08/64] [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 2309cb406..5a1627686 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -16,8 +16,8 @@ import sys 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 09/64] 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 ef070b6c5..3b11c1123 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -141,6 +141,16 @@ class TestFileJpeg: ) 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 @@ class TestFileJpeg: 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 23da312a6..46998e03a 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 5add65f45..00548e8b7 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -781,6 +781,7 @@ def _save(im, fp, filename): 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 4664ad0f3..c7dd51015 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 5cc74e69b..98eaac28d 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 9da830b18..2946cc530 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 10/64] Rearranged subsampling assertions --- Tests/test_file_jpeg.py | 43 +++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3b11c1123..e270820bd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -145,11 +145,13 @@ class TestFileJpeg: 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 @@ class TestFileJpeg: # 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") - 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) + with pytest.raises(TypeError): + self.roundtrip(hopper(), subsampling="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 11/64] 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 e270820bd..748c07119 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -434,8 +434,9 @@ class TestFileJpeg: 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 @@ class TestFileJpeg: 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 12/64] 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 748c07119..75851e477 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -447,9 +447,15 @@ class TestFileJpeg: 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 46998e03a..773d517ef 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 2946cc530..00f3d5f74 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 13/64] 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 9883f10ba..284de7f4f 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 6bcf807fe2cb82c859fb757a498edfe5692ededa Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 00:17:57 +0100 Subject: [PATCH 14/64] 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 193e8c9b2..0c25edeb3 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 4634d335b..d11cbfa58 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 15/64] 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 1457d85f7..4a312beb4 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 0c25edeb3..ef0ff8a8e 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 d04801cba..5a2e85419 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -92,7 +92,7 @@ try: 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 9d27f2513..643fce830 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -28,7 +28,7 @@ except ImportError as ex: # 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 6db7cc4ec..41d8fbc17 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,7 @@ import warnings 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 @@ try: 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 23ff154f6..07bb712d8 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -43,7 +43,7 @@ except ImportError as ex: # 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 000000000..b0235555d --- /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 000000000..b0235555d --- /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 16/64] 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 ef0ff8a8e..de707a283 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 ae4e23db1..0923979af 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -32,7 +32,7 @@ import io 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 17/64] 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 363a67d9b..57dd3f35b 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 de707a283..abb32f14d 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 000000000..583be41b7 --- /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 d11cbfa58..37cd979f6 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 034d89372..bfcfaf5d9 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 18/64] [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 583be41b7..608b2b41f 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 @@ else: 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 37cd979f6..13f369cca 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -3,6 +3,7 @@ from __future__ import annotations import os from pathlib import Path from typing import Any, NoReturn + from ._typing import TypeGuard diff --git a/tox.ini b/tox.ini index bfcfaf5d9..d89d017e4 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 19/64] 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 f71b6b1a2..d7095dce3 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 20/64] Coverage: Use exclude_also instead of exclude_lines --- .coveragerc | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index d7095dce3..1c5e7654e 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 21/64] Don't complain about compatibility code: class TypeGuard --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 1c5e7654e..f18c5ea52 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 22/64] 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 57dd3f35b..f2932c322 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 372083c59f06c37564be040a1d4b8cd1dc5bd0ac Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Thu, 28 Dec 2023 13:00:17 -0600 Subject: [PATCH 23/64] 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 75851e477..5513b1403 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -148,6 +148,7 @@ class TestFileJpeg: 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 f6bcf4e1ae3184ff9ed0cfcb46a0aa80423da537 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 14:15:40 +1100 Subject: [PATCH 24/64] 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 0851796d0..73a1223d7 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -484,13 +484,13 @@ class TestFileTiff: 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 @@ class TestFileTiff: 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 edd57e6b5..ed90031fa 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 fc242ca64..f9da3e649 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 f7ec665bf1656245e9e3b483ff92e9eb1e0ef465 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 22:59:43 +1100 Subject: [PATCH 25/64] 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 0851796d0..0737c8a2a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -612,6 +612,14 @@ class TestFileTiff: 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 edd57e6b5..1a814d97e 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 fc242ca64..e9db64e5f 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 ee6e12a8032a8255b2f7a60a40c50377c17a03fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 18:37:16 +1100 Subject: [PATCH 26/64] 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 cf65e86d7..0069eb5bc 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 bc44ba388..73af6d99c 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 a70dece74..9ae7ae605 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,7 +54,7 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" copyright = ( - "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" + "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 27/64] 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 d0ecde393..e1a8c92c7 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,6 +11,15 @@ from .helper import hopper 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 e7dc3e4e4..09a60f25c 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -128,24 +128,20 @@ class IptcImageFile(ImageFile.ImageFile): # 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 46a6ddf0c2eb36a06f9952a4e8d98ff2183198f6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 31 Dec 2023 13:47:37 +0100 Subject: [PATCH 28/64] 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 e1a8c92c7..04313ed85 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -6,18 +6,21 @@ import pytest 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 09a60f25c..7b6123c66 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 @@ class IptcImageFile(ImageFile.ImageFile): 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 @@ class IptcImageFile(ImageFile.ImageFile): 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 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 29/64] 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 04313ed85..075a461af 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -12,7 +12,7 @@ TEST_FILE = "Tests/images/iptc.jpg" 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 fa4b3776f0dda812a1cea15f3ba17a3777b8c5b6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 30 Dec 2023 00:02:34 +0100 Subject: [PATCH 30/64] =?UTF-8?q?=EF=BB=BFdeprecate=20IptcImagePlugin.{dum?= =?UTF-8?q?p,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 e1a8c92c7..3960e027a 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 3a028de2d..deac39e25 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -22,25 +22,38 @@ import tempfile 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 @@ class IptcImageFile(ImageFile.ImageFile): 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 31/64] =?UTF-8?q?=EF=BB=BFdocument=20IptcImagePlugin=20dep?= =?UTF-8?q?recations?= 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 | 10 +++++++--- src/PIL/IptcImagePlugin.py | 2 ++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 75c0b73eb..a42dc555f 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 6ab139b56..0244b6f77 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 -^^^^ +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +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 deac39e25..faf3ed936 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 32/64] =?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 613d9462a..055dbda7a 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 faf3ed936..2314ddce8 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import os import tempfile +from typing import Sequence from . import Image, ImageFile from ._binary import i16be as i16 @@ -27,7 +28,7 @@ from ._deprecate import deprecate 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 @@ class IptcImageFile(ImageFile.ImageFile): 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 33/64] 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 fa036b9cc..493d0bde3 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 6ab139b56..f82d20176 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 34/64] 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 f82d20176..c08a85c2c 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 35/64] 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 f18c5ea52..70dd4e57f 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 36/64] Remove unused create_lut() --- .coveragerc | 2 -- Tests/test_imagemorph.py | 9 --------- 2 files changed, 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index 70dd4e57f..46df3f90d 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 ec55aadf9..64a1785ea 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 37/64] Update CHANGES.rst [ci skip] --- CHANGES.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1dc8e9aaa..2aeae2d5c 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 38/64] 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 c08a85c2c..46cfe1599 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 39/64] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 2aeae2d5c..6da7b28d4 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 40/64] 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 7b6123c66..6b5e34c4f 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 @@ def getiptcinfo(im): # 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 41/64] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6da7b28d4..6062ad742 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 42/64] 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 021b40c0e..035b83c4d 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -396,7 +396,7 @@ class Color3DLUT(MultibandFilter): 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 43/64] 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 7f0366ddb..78c6b9eca 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -150,6 +150,7 @@ class ImageFont: :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 44/64] 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 0244b6f77..4e7c58756 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 2270c77fe..e06780c75 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 45/64] 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 21b4dee3c..fd07ee23b 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 46/64] 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 e06780c75..8b5cac180 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 47/64] 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 8b5cac180..e0e5f804a 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 48/64] 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 0244b6f77..ca5750734 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 49/64] 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 ca5750734..4ed8df98a 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 50/64] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6062ad742..3c8b4d115 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 51/64] 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 493d0bde3..b617d9205 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 52/64] 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 b617d9205..b6944e234 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 46cfe1599..75ed461ba 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 53/64] 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 055dbda7a..065b22dc9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2643,7 +2643,7 @@ class Image: 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 1fdaa9140..1dc3e210e 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 54/64] 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 f8ef4b8c8..d1f549e9c 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 55/64] 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 1dc3e210e..84c81f184 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -20,7 +20,7 @@ from . import Image 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 56/64] 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 fd07ee23b..9e0855101 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 e0e5f804a..59f80a354 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 57/64] Update CHANGES.rst [ci skip] --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3c8b4d115..006b6d567 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 58/64] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 006b6d567..1e872d172 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 59/64] 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 d79f2465f..523e2ad74 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 60/64] [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 d1c4b8015..6adc75b49 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 6956d0b2853f5c7ec5f6ec4c60725c5a7ee73aeb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 18:32:31 +1100 Subject: [PATCH 61/64] 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 1e872d172..85036f642 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 7d994caf4..1018b96b5 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 dacd92853094414ab6a4326e7f5805666b92996c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:37:59 +1100 Subject: [PATCH 62/64] 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 1018b96b5..0568943b5 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 75015e9859183e426012aa59309473c479b59a59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 22:43:56 +1100 Subject: [PATCH 63/64] 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 060fc497e..5adff7ec1 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 64/64] 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 b3fd72a52..97f4f8dcd 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