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/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff
index 9b9eda883..f0cb534b6 100644
Binary files a/Tests/images/imagedraw_rectangle_I.tiff and b/Tests/images/imagedraw_rectangle_I.tiff differ
diff --git a/Tests/images/op_index.qoi b/Tests/images/op_index.qoi
new file mode 100644
index 000000000..e626aafe6
Binary files /dev/null and b/Tests/images/op_index.qoi differ
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/Tests/test_file_qoi.py b/Tests/test_file_qoi.py
index 25cd20748..4222d2b94 100644
--- a/Tests/test_file_qoi.py
+++ b/Tests/test_file_qoi.py
@@ -32,6 +32,12 @@ def test_invalid_file() -> None:
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)
+
+
def test_save(tmp_path: Path) -> None:
f = tmp_path / "temp.qoi"
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/Tests/test_image.py b/Tests/test_image.py
index 4cc841603..b018b4309 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:
@@ -990,7 +995,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/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index ffe9c0979..37669a2e5 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -783,9 +783,10 @@ def test_rectangle_I16(bbox: Coords) -> 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/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/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
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index ed2f728aa..7e9540e48 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:
@@ -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])
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 299405ae0..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,6 +66,8 @@ class PcxImageFile(ImageFile.ImageFile):
raise SyntaxError(msg)
logger.debug("BBox: %s %s %s %s", *bbox)
+ offset = self.fp.tell() + 60
+
# 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))]
# --------------------------------------------------------------------
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:
diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py
index c123249ea..aab0533e3 100644
--- a/src/PIL/QoiImagePlugin.py
+++ b/src/PIL/QoiImagePlugin.py
@@ -54,7 +54,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)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 88af9162e..946fbd531 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:
@@ -1259,7 +1260,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()
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++;
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);
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",