From e10a58ccce18c3493e30dfe7222076964311a8d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Oct 2023 21:46:38 +1100 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] [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 f6bcf4e1ae3184ff9ed0cfcb46a0aa80423da537 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 14:15:40 +1100 Subject: [PATCH 09/21] 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 10/21] 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 6a33d6d170d51419ea63e905637c98f5e6db1af9 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 29 Dec 2023 23:15:41 +0100 Subject: [PATCH 11/21] 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 9bb4260a4..b8b871df6 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,16 +18,16 @@ from __future__ import annotations 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 12/21] 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 64d042426..0035296a4 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 @@ class ContainerIO: :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 @@ class ContainerIO: ## # 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 @@ class ContainerIO: 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 @@ class ContainerIO: """ return self.pos - def read(self, n=0): + def read(self, n: int = 0) -> AnyStr: """ Read data. @@ -84,17 +85,17 @@ class ContainerIO: 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 @@ class ContainerIO: 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 c9923487d..7470663b4 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -21,7 +21,7 @@ from types import TracebackType 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 ee6e12a8032a8255b2f7a60a40c50377c17a03fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 18:37:16 +1100 Subject: [PATCH 13/21] 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 2ec53e36e9135fda4e2eb6bbd5343a7facae71da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 23:17:28 +1100 Subject: [PATCH 14/21] 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 efe523643..807d581ed 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 ade152fcd..6ab139b56 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 6db7cc4ec..7f0366ddb 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -149,6 +149,7 @@ class ImageFont: :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 f410ec4eab314653bcbc91a9d0ab754fa331972e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 00:21:25 +1100 Subject: [PATCH 15/21] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c5c3ea9c7..1dc8e9aaa 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 3396ce102d0a2e8db663522001cab9786edf19c0 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 29 Dec 2023 23:18:08 +0100 Subject: [PATCH 16/21] 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 e7dc3e4e4..cc39441b6 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -20,26 +20,29 @@ import os 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 @@ class IptcImageFile(ImageFile.ImageFile): 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 b8b871df6..0a07e8d0e 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,8 +18,8 @@ from __future__ import annotations 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 17/21] =?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 18/21] =?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 19/21] =?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 9e835ca5be86818bdfce583cd4f544f2cb741db5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 13:51:45 +1100 Subject: [PATCH 20/21] 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 ca48ac0e15075a0a986826f094c2b68aa8f4ac26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 14:31:07 +1100 Subject: [PATCH 21/21] 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]