Merge branch 'main' into qoi_write

This commit is contained in:
Andrew Murray 2025-06-11 22:58:13 +10:00 committed by GitHub
commit 2f5137fdce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 101 additions and 34 deletions

View File

@ -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

BIN
Tests/images/op_index.qoi Normal file

Binary file not shown.

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00'
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00 '
)
if ElementTree is None:
with pytest.warns(

View File

@ -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")

View File

@ -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")

View File

@ -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

View File

@ -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])

View File

@ -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))]
# --------------------------------------------------------------------

View File

@ -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:

View File

@ -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)

View File

@ -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()

View File

@ -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++;

View File

@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) {
}
}
im->read_only = view.readonly;
im->destroy = mapping_destroy_buffer;
Py_INCREF(target);

View File

@ -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",