Merge branch 'main' into avif_gray

This commit is contained in:
Andrew Murray 2026-03-28 06:57:04 +11:00
commit 15f13ef46e
15 changed files with 228 additions and 73 deletions

View File

@ -104,7 +104,7 @@ jobs:
cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator"
platform: ios
os: macos-26-intel
os: macos-15-intel
cibw_arch: x86_64_iphonesimulator
steps:
- uses: actions/checkout@v6

View File

@ -6,7 +6,14 @@ from typing import Any
import pytest
from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
from PIL import (
Image,
ImageFile,
JpegImagePlugin,
MpoImagePlugin,
TiffImagePlugin,
_binary,
)
from .helper import (
assert_image_equal,
@ -145,6 +152,32 @@ def test_parallax() -> None:
assert exif.get_ifd(0x927C)[0xB211] == -3.125
def test_truncated_makernote() -> None:
def check(ifd: TiffImagePlugin.ImageFileDirectory_v2) -> None:
fp = BytesIO()
ifd.save(fp)
e = Image.Exif()
e.load(fp.getvalue())
assert e.get_ifd(37500) == {}
# Nintendo
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[271] = "Nintendo"
ifd[34665] = {37500: b" "}
check(ifd)
# Fujifilm
for data in (
b"FUJIFILM",
b"FUJIFILM" + _binary.o32le(50),
b"FUJIFILM" + _binary.o32le(0),
):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[34665] = {37500: data}
check(ifd)
def test_reload_exif_after_seek() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
exif = im.getexif()

View File

@ -16,6 +16,7 @@ from PIL import (
TiffImagePlugin,
TiffTags,
UnidentifiedImageError,
_binary,
)
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
@ -941,6 +942,15 @@ class TestFileTiff:
4001,
]
def test_truncated_photoshop_blocks(self) -> None:
with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[34377] = b"8BIM"
assert im.get_photoshop_blocks() == {}
im.tag_v2[34377] = b"8BIM" + _binary.o16be(0) + _binary.o8(2) + b" " * 5
assert im.get_photoshop_blocks() == {}
def test_tiff_chunks(self, tmp_path: Path) -> None:
tmpfile = tmp_path / "temp.tif"

View File

@ -4234,80 +4234,83 @@ class Exif(_ExifBase):
if tag == ExifTags.IFD.MakerNote:
from .TiffImagePlugin import ImageFileDirectory_v2
if tag_data.startswith(b"FUJIFILM"):
ifd_offset = i32le(tag_data, 8)
ifd_data = tag_data[ifd_offset:]
try:
if tag_data.startswith(b"FUJIFILM"):
ifd_offset = i32le(tag_data, 8)
ifd_data = tag_data[ifd_offset:]
makernote = {}
for i in range(struct.unpack("<H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
try:
(
unit_size,
handler,
) = ImageFileDirectory_v2._load_dispatch[typ]
except KeyError:
continue
size = count * unit_size
if size > 4:
(offset,) = struct.unpack("<L", data)
data = ifd_data[offset - 12 : offset + size - 12]
else:
data = data[:size]
if len(data) != size:
warnings.warn(
"Possibly corrupt EXIF MakerNote data. "
f"Expecting to read {size} bytes but only got "
f"{len(data)}. Skipping tag {ifd_tag}"
makernote = {}
for i in range(struct.unpack("<H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
continue
try:
(
unit_size,
handler,
) = ImageFileDirectory_v2._load_dispatch[typ]
except KeyError:
continue
size = count * unit_size
if size > 4:
(offset,) = struct.unpack("<L", data)
data = ifd_data[offset - 12 : offset + size - 12]
else:
data = data[:size]
if not data:
continue
if len(data) != size:
warnings.warn(
"Possibly corrupt EXIF MakerNote data. "
f"Expecting to read {size} bytes but only got "
f"{len(data)}. Skipping tag {ifd_tag}"
)
continue
makernote[ifd_tag] = handler(
ImageFileDirectory_v2(), data, False
)
self._ifds[tag] = dict(self._fixup_dict(makernote))
elif self.get(0x010F) == "Nintendo":
makernote = {}
for i in range(struct.unpack(">H", tag_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
if ifd_tag == 0x1101:
# CameraInfo
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)
if not data:
continue
camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
makernote[ifd_tag] = handler(
ImageFileDirectory_v2(), data, False
)
self._ifds[tag] = dict(self._fixup_dict(makernote))
elif self.get(0x010F) == "Nintendo":
makernote = {}
for i in range(struct.unpack(">H", tag_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
if ifd_tag == 0x1101:
# CameraInfo
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)
self.fp.read(4)
# Seconds since 2000
camerainfo["TimeStamp"] = i32le(self.fp.read(12))
camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
self.fp.read(4)
camerainfo["InternalSerialNumber"] = self.fp.read(4)
self.fp.read(4)
# Seconds since 2000
camerainfo["TimeStamp"] = i32le(self.fp.read(12))
self.fp.read(12)
parallax = self.fp.read(4)
handler = ImageFileDirectory_v2._load_dispatch[
TiffTags.FLOAT
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)[0]
self.fp.read(4)
camerainfo["InternalSerialNumber"] = self.fp.read(4)
self.fp.read(4)
camerainfo["Category"] = self.fp.read(2)
self.fp.read(12)
parallax = self.fp.read(4)
handler = ImageFileDirectory_v2._load_dispatch[
TiffTags.FLOAT
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)[0]
makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote
self.fp.read(4)
camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote
except struct.error:
pass
else:
# Interop
ifd = self._get_ifd_dict(tag_data, tag)

View File

@ -1287,10 +1287,13 @@ class TiffImageFile(ImageFile.ImageFile):
blocks = {}
val = self.tag_v2.get(ExifTags.Base.ImageResources)
if val:
while val.startswith(b"8BIM"):
while val.startswith(b"8BIM") and len(val) >= 12:
id = i16(val[4:6])
n = math.ceil((val[6] + 1) / 2) * 2
size = i32(val[6 + n : 10 + n])
try:
size = i32(val[6 + n : 10 + n])
except struct.error:
break
data = val[10 + n : 10 + n + size]
blocks[id] = {"data": data}

View File

@ -721,17 +721,29 @@ _decoder_get_info(AvifDecoderObject *self) {
mode = "RGB";
}
if (image->icc.size) {
icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size);
if (image->xmp.size) {
xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size);
if (!xmp) {
return NULL;
}
}
if (image->exif.size) {
exif =
PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size);
if (!exif) {
Py_XDECREF(xmp);
return NULL;
}
}
if (image->xmp.size) {
xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size);
if (image->icc.size) {
icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size);
if (!icc) {
Py_XDECREF(xmp);
Py_XDECREF(exif);
return NULL;
}
}
ret = Py_BuildValue(
@ -822,6 +834,7 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) {
PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
avifRGBImageFreePixels(&rgb);
return NULL;
}
@ -829,6 +842,9 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size);
avifRGBImageFreePixels(&rgb);
if (!bytes) {
return NULL;
}
ret = Py_BuildValue(
"SKKK",

View File

@ -254,6 +254,9 @@ void
ReleaseArrowSchemaPyCapsule(PyObject *capsule) {
struct ArrowSchema *schema =
(struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema");
if (!schema) {
return;
}
if (schema->release != NULL) {
schema->release(schema);
}
@ -276,6 +279,9 @@ void
ReleaseArrowArrayPyCapsule(PyObject *capsule) {
struct ArrowArray *array =
(struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array");
if (!array) {
return;
}
if (array->release != NULL) {
array->release(array);
}
@ -2473,6 +2479,9 @@ _split(ImagingObject *self) {
}
list = PyTuple_New(self->image->bands);
if (!list) {
return NULL;
}
for (i = 0; i < self->image->bands; i++) {
imaging_object = PyImagingNew(bands[i]);
if (!imaging_object) {
@ -3769,6 +3778,9 @@ _ptr_destructor(PyObject *capsule) {
static PyObject *
_getattr_ptr(ImagingObject *self, void *closure) {
PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor);
if (!capsule) {
return NULL;
}
Py_INCREF(self);
PyCapsule_SetContext(capsule, self);
return capsule;

View File

@ -558,7 +558,13 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) {
}
im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!im) {
return NULL;
}
imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
if (!imOut) {
return NULL;
}
return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform));
}

View File

@ -941,8 +941,18 @@ font_render(FontObject *self, PyObject *args) {
return NULL;
}
PyObject *imagePtr = PyObject_GetAttrString(image, "ptr");
if (!imagePtr) {
PyMem_Del(glyph_info);
Py_DECREF(image);
return NULL;
}
im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC);
Py_XDECREF(imagePtr);
Py_DECREF(imagePtr);
if (!im) {
PyMem_Del(glyph_info);
Py_DECREF(image);
return NULL;
}
x_offset = round(x_offset - stroke_width);
y_offset = round(y_offset - stroke_width);

View File

@ -187,8 +187,17 @@ _unop(PyObject *self, PyObject *args) {
}
unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC);
if (!unop) {
return NULL;
}
out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!out) {
return NULL;
}
im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
if (!im1) {
return NULL;
}
unop(out, im1);
@ -219,9 +228,21 @@ _binop(PyObject *self, PyObject *args) {
}
binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC);
if (!binop) {
return NULL;
}
out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!out) {
return NULL;
}
im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
if (!im1) {
return NULL;
}
im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC);
if (!im2) {
return NULL;
}
binop(out, im1, im2);

View File

@ -53,7 +53,13 @@ apply(PyObject *self, PyObject *args) {
}
imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!imgin) {
return NULL;
}
imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC);
if (!imgout) {
return NULL;
}
width = imgin->xsize;
height = imgin->ysize;
@ -143,6 +149,9 @@ match(PyObject *self, PyObject *args) {
}
imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!imgin) {
return NULL;
}
if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) {
PyErr_SetString(PyExc_RuntimeError, "Unsupported image type");
@ -185,6 +194,10 @@ match(PyObject *self, PyObject *args) {
(b6 << 6) | (b7 << 7) | (b8 << 8));
if (lut[lut_idx]) {
PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx);
if (!coordObj) {
Py_DECREF(ret);
return NULL;
}
PyList_Append(ret, coordObj);
Py_XDECREF(coordObj);
}
@ -216,6 +229,9 @@ get_on_pixels(PyObject *self, PyObject *args) {
}
img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!img) {
return NULL;
}
rows = img->image8;
width = img->xsize;
height = img->ysize;
@ -230,6 +246,10 @@ get_on_pixels(PyObject *self, PyObject *args) {
for (col_idx = 0; col_idx < width; col_idx++) {
if (row[col_idx]) {
PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx);
if (!coordObj) {
Py_DECREF(ret);
return NULL;
}
PyList_Append(ret, coordObj);
Py_XDECREF(coordObj);
}

View File

@ -262,6 +262,9 @@ _anim_encoder_add(PyObject *self, PyObject *args) {
}
im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!im) {
return NULL;
}
// Setup config for this frame
if (!WebPConfigInit(&config)) {
@ -505,6 +508,9 @@ _anim_decoder_get_next(PyObject *self) {
bytes = PyBytes_FromStringAndSize(
(char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height
);
if (!bytes) {
return NULL;
}
ret = Py_BuildValue("Si", bytes, timestamp);
@ -607,6 +613,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
}
im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC);
if (!im) {
return NULL;
}
// Setup config for this frame
if (!WebPConfigInit(&config)) {

View File

@ -480,6 +480,9 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
GlobalUnlock(handle);
CloseClipboard();
if (!result) {
return NULL;
}
return Py_BuildValue("zN", format_names[format], result);
}

View File

@ -677,9 +677,15 @@ ImagingNewArrow(
Imaging im;
struct ArrowSchema *schema =
(struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema");
if (!schema) {
return NULL;
}
struct ArrowArray *external_array =
(struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array");
if (!external_array) {
return NULL;
}
if (xsize < 0 || ysize < 0) {
return (Imaging)ImagingError_ValueError("bad image size");

View File

@ -41,6 +41,9 @@ _imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) {
PyObject *byteObj;
byteObj = PyBytes_FromStringAndSize(src, bytes);
if (!byteObj) {
return -1;
}
result = PyObject_CallMethod(fd, "write", "O", byteObj);
Py_DECREF(byteObj);