From 041acf13440f9307dcc129eff21bcd065362ae91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 May 2025 15:00:47 +1000 Subject: [PATCH 01/12] Clear core image if memory mapping was used for last load --- Tests/test_tiff_crashes.py | 14 ++++++++++++++ src/PIL/TiffImagePlugin.py | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 073e5415c..976f62384 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None: pytest.skip("test image not found") except OSError: pass + + +def test_tiff_mmap() -> None: + try: + with Image.open("Tests/images/crash_mmap.tif") as im: + im.seek(1) + im.load() + + im.seek(0) + im.load() + except FileNotFoundError: + if on_ci(): + raise + pytest.skip("test image not found") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e..5cbac0c26 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1217,9 +1217,10 @@ class TiffImageFile(ImageFile.ImageFile): return self._seek(frame) if self._im is not None and ( - self.im.size != self._tile_size or self.im.mode != self.mode + self.im.size != self._tile_size + or self.im.mode != self.mode + or self.readonly ): - # The core image will no longer be used self._im = None def _seek(self, frame: int) -> None: From eff667a8614ba3b567684597795924387c8cbaaa Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 23 May 2025 10:22:59 +0100 Subject: [PATCH 02/12] Mark the image read-only in the C layer if it's created from a read only buffer --- src/map.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/map.c b/src/map.c index c66702981..9a3144ab9 100644 --- a/src/map.c +++ b/src/map.c @@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { } } + im->read_only = view.readonly; im->destroy = mapping_destroy_buffer; Py_INCREF(target); From cb077a16c80e9d23bb3976182acae7fc090aa5dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jun 2025 20:07:13 +1000 Subject: [PATCH 03/12] Handle UNDEFINED XMP data --- Tests/test_file_tiff.py | 24 ++++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 5 ++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index d0d394aa9..73046eb5f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -14,6 +14,7 @@ from PIL import ( ImageFile, JpegImagePlugin, TiffImagePlugin, + TiffTags, UnidentifiedImageError, ) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -900,6 +901,29 @@ class TestFileTiff: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_getxmp_undefined(self, tmp_path: Path) -> None: + tmpfile = tmp_path / "temp.tif" + im = Image.new("L", (1, 1)) + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd.tagtype[700] = TiffTags.UNDEFINED + with Image.open("Tests/images/lab.tif") as im_xmp: + ifd[700] = im_xmp.info["xmp"] + im.save(tmpfile, tiffinfo=ifd) + + with Image.open(tmpfile) as im_reloaded: + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + assert im_reloaded.getxmp() == {} + else: + assert "xmp" in im_reloaded.info + xmp = im_reloaded.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 88af9162e..22c5208e2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1259,7 +1259,10 @@ class TiffImageFile(ImageFile.ImageFile): self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) if XMP in self.tag_v2: - self.info["xmp"] = self.tag_v2[XMP] + xmp = self.tag_v2[XMP] + if isinstance(xmp, tuple) and len(xmp) == 1: + xmp = xmp[0] + self.info["xmp"] = xmp elif "xmp" in self.info: del self.info["xmp"] self._reload_exif() From f03c23683ed83a9d8f73e73073ac28f1ab2b74ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 4 Jun 2025 20:08:58 +1000 Subject: [PATCH 04/12] Trim whitespace from end when parsing XMP data --- Tests/test_image.py | 2 +- src/PIL/Image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 14a067127..ac358f5bf 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -989,7 +989,7 @@ class TestImage: im = Image.new("RGB", (1, 1)) im.info["xmp"] = ( b'\n' - b'\n\x00\x00' + b'\n\x00\x00 ' ) if ElementTree is None: with pytest.warns( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed2f728aa..e03e9cc8a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1511,7 +1511,7 @@ class Image: return {} if "xmp" not in self.info: return {} - root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00")) + root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) return {get_name(root.tag): get_value(root)} def getexif(self) -> Exif: From 313969cf0bcf6b6185d486830478d2864eb56fe1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Jun 2025 12:21:49 +1000 Subject: [PATCH 05/12] Removed unnecessary seek --- src/PIL/PcxImagePlugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 299405ae0..47b6e80e2 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -66,6 +66,8 @@ class PcxImageFile(ImageFile.ImageFile): raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) + offset = self.fp.tell() + # format version = s[1] bits = s[3] @@ -102,7 +104,6 @@ class PcxImageFile(ImageFile.ImageFile): break if mode == "P": self.palette = ImagePalette.raw("RGB", s[1:]) - self.fp.seek(128) elif version == 5 and bits == 8 and planes == 3: mode = "RGB" @@ -128,9 +129,7 @@ class PcxImageFile(ImageFile.ImageFile): bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) - self.tile = [ - ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride)) - ] + self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))] # -------------------------------------------------------------------- From 7341e70f6be9c3e910c81f563bb7900167873c02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Jun 2025 12:20:52 +1000 Subject: [PATCH 06/12] Reduced number of bytes read for header --- src/PIL/PcxImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 47b6e80e2..458d586c4 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,7 +54,7 @@ class PcxImageFile(ImageFile.ImageFile): # header assert self.fp is not None - s = self.fp.read(128) + s = self.fp.read(68) if not _accept(s): msg = "not a PCX file" raise SyntaxError(msg) @@ -66,7 +66,7 @@ class PcxImageFile(ImageFile.ImageFile): raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) - offset = self.fp.tell() + offset = self.fp.tell() + 60 # format version = s[1] From e65e5bea45e92a118590c69c022d6e6741e3b101 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Jun 2025 20:30:18 +1000 Subject: [PATCH 07/12] Start decoding with a zero-initialized array of previously seen pixels --- Tests/images/op_index.qoi | Bin 0 -> 15 bytes Tests/test_file_qoi.py | 6 ++++++ src/PIL/QoiImagePlugin.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Tests/images/op_index.qoi diff --git a/Tests/images/op_index.qoi b/Tests/images/op_index.qoi new file mode 100644 index 0000000000000000000000000000000000000000..e626aafe6a433487cbacb4bdbcbbe5e66e8d07db GIT binary patch literal 15 TcmXTS&rD-rU| None: with pytest.raises(SyntaxError): QoiImagePlugin.QoiImageFile(invalid_file) + + +def test_op_index() -> None: + # QOI_OP_INDEX as the first chunk + with Image.open("Tests/images/op_index.qoi") as im: + assert im.getpixel((0, 0)) == (0, 0, 0, 0) diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index df552243e..75070abd7 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -51,7 +51,7 @@ class QoiDecoder(ImageFile.PyDecoder): assert self.fd is not None self._previously_seen_pixels = {} - self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) + self._previous_pixel = bytearray((0, 0, 0, 255)) data = bytearray() bands = Image.getmodebands(self.mode) From 646885e546ecd02a8162d91b51d32eed9da67b7a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:06:28 +1000 Subject: [PATCH 08/12] Parse XMP tag bytes without decoding to string (#8960) Co-authored-by: Andrew Murray --- Tests/test_image.py | 5 +++++ src/PIL/Image.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 4cc841603..512a52433 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -974,6 +974,11 @@ class TestImage: assert tag not in exif.get_ifd(0x8769) assert exif.get_ifd(0xA005) + def test_exif_from_xmp_bytes(self) -> None: + im = Image.new("RGB", (1, 1)) + im.info["xmp"] = b'\xff tiff:Orientation="2"' + assert im.getexif()[274] == 2 + def test_empty_xmp(self) -> None: with Image.open("Tests/images/hopper.gif") as im: if ElementTree is None: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ed2f728aa..216022565 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1542,10 +1542,11 @@ class Image: # XMP tags if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") + pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' if not xmp_tags and (xmp_tags := self.info.get("xmp")): - xmp_tags = xmp_tags.decode("utf-8") + pattern = rb'tiff:Orientation(="|>)([0-9])' if xmp_tags: - match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) + match = re.search(pattern, xmp_tags) if match: self._exif[ExifTags.Base.Orientation] = int(match[2]) From 36cea1953231d71f1184ef1396c1f01ff11c939a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:08:29 +1000 Subject: [PATCH 09/12] Do not decode bytes in PPM error message (#8958) --- Tests/test_file_ppm.py | 7 ++++--- src/PIL/PpmImagePlugin.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 41e2b5416..c7d1f4df4 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -288,12 +288,13 @@ def test_non_integer_token(tmp_path: Path) -> None: pass -def test_header_token_too_long(tmp_path: Path) -> None: +@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890")) +def test_header_token_too_long(tmp_path: Path, data: bytes) -> None: path = tmp_path / "temp.ppm" with open(path, "wb") as f: - f.write(b"P6\n 01234567890") + f.write(data) - with pytest.raises(ValueError, match="Token too long in file header: 01234567890"): + with pytest.raises(ValueError, match="Token too long in file header: "): with Image.open(path): pass diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 03afa2d2e..db34d107a 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -94,8 +94,8 @@ class PpmImageFile(ImageFile.ImageFile): msg = "Reached EOF while reading header" raise ValueError(msg) elif len(token) > 10: - msg = f"Token too long in file header: {token.decode()}" - raise ValueError(msg) + msg_too_long = b"Token too long in file header: %s" % token + raise ValueError(msg_too_long) return token def _open(self) -> None: From d7a45cc250f8ae35ee8095753eff0cad1c9f8216 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:57:37 +1000 Subject: [PATCH 10/12] ImageFont does not handle multiline text (#9000) --- docs/reference/ImageFont.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 8b2f92323..aac55fe6b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -18,6 +18,9 @@ OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of the imToolkit package. +When measuring text sizes, this module will not break at newline characters. For +multiline text, see the :py:mod:`~PIL.ImageDraw` module. + .. warning:: To protect against potential DOS attacks when using arbitrary strings as text input, Pillow will raise a :py:exc:`ValueError` if the number of characters From 056dc89a3c85cbd6d6c960cbfc5aaa52f996bd3d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:12:40 +1000 Subject: [PATCH 11/12] Correct drawing I;16 horizontal lines (#8985) --- Tests/images/imagedraw_rectangle_I.tiff | Bin 20122 -> 20122 bytes Tests/test_imagedraw.py | 3 ++- src/libImaging/Draw.c | 34 +++++++++++++++--------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff index 9b9eda883a371d9cc88b4677b09d2e351c42e609..f0cb534b63e47c940ecb6c3323cb9de3dce573d4 100644 GIT binary patch literal 20122 zcmeI&F%AJy7=_V)j0hbKjY4fF8mq7hdz`h*7CbV=ls6F~a){*Rweqr`|14bRhJA-KYQ None: draw = ImageDraw.Draw(im) # Act - draw.rectangle(bbox, outline=0xFFFF) + draw.rectangle(bbox, outline=0xCDEF) # Assert + assert im.getpixel((X0, Y0)) == 0xCDEF assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 70f267ae4..27cac687e 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -104,8 +104,6 @@ point32rgba(Imaging im, int x, int y, int ink) { static inline void hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { - int pixelwidth; - if (y0 >= 0 && y0 < im->ysize) { if (x0 < 0) { x0 = 0; @@ -118,20 +116,30 @@ hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) { x1 = im->xsize - 1; } if (x0 <= x1) { - pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1; - if (mask == NULL) { - memset( - im->image8[y0] + x0 * pixelwidth, - (UINT8)ink, - (x1 - x0 + 1) * pixelwidth - ); + int bigendian = -1; + if (strncmp(im->mode, "I;16", 4) == 0) { + bigendian = + ( +#ifdef WORDS_BIGENDIAN + strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0 +#else + strcmp(im->mode, "I;16B") == 0 +#endif + ) + ? 1 + : 0; + } + if (mask == NULL && bigendian == -1) { + memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1)); } else { UINT8 *p = im->image8[y0]; while (x0 <= x1) { - if (mask->image8[y0][x0]) { - p[x0 * pixelwidth] = ink; - if (pixelwidth == 2) { - p[x0 * pixelwidth + 1] = ink; + if (mask == NULL || mask->image8[y0][x0]) { + if (bigendian == -1) { + p[x0] = ink; + } else { + p[x0 * 2 + (bigendian ? 1 : 0)] = ink; + p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8; } } x0++; From 3eb893f0c16c958962758c03a597454aacac8f84 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:56:28 +1000 Subject: [PATCH 12/12] Updated libjpeg-turbo to 3.1.1 (#9009) --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1583435c1..b46811f5a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 LIBPNG_VERSION=1.6.48 -JPEGTURBO_VERSION=3.1.0 +JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 6e176e29c..0cc383733 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,7 +114,7 @@ V = { "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "11.2.1", - "JPEGTURBO": "3.1.0", + "JPEGTURBO": "3.1.1", "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4",