Skip to content

Commit 330e341

Browse files
committed
Added type hints
1 parent 2ed8502 commit 330e341

File tree

7 files changed

+74
-44
lines changed

7 files changed

+74
-44
lines changed

Tests/test_file_tiff.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,13 @@ def test_planar_configuration_save(self, tmp_path: Path) -> None:
684684
with Image.open(outfile) as reloaded:
685685
assert_image_equal_tofile(reloaded, infile)
686686

687+
def test_invalid_tiled_dimensions(self) -> None:
688+
with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
689+
data = fp.read()
690+
b = BytesIO(data[:144] + b"\x02" + data[145:])
691+
with pytest.raises(ValueError):
692+
Image.open(b)
693+
687694
@pytest.mark.parametrize("mode", ("P", "PA"))
688695
def test_palette(self, mode: str, tmp_path: Path) -> None:
689696
outfile = str(tmp_path / "temp.tif")

Tests/test_imagefile.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,8 @@ def test_encode(self) -> None:
412412
with pytest.raises(NotImplementedError):
413413
encoder.encode_to_pyfd()
414414

415-
fh = BytesIO()
416415
with pytest.raises(NotImplementedError):
417-
encoder.encode_to_file(fh, 0)
416+
encoder.encode_to_file(0, 0)
418417

419418
def test_zero_height(self) -> None:
420419
with pytest.raises(UnidentifiedImageError):

docs/reference/Image.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ Classes
363363
:show-inheritance:
364364
.. autoclass:: PIL.Image.ImagePointHandler
365365
.. autoclass:: PIL.Image.ImageTransformHandler
366+
.. autoclass:: PIL.Image._E
366367

367368
Protocols
368369
---------

src/PIL/IcoImagePlugin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,11 @@ def _open(self) -> None:
319319
self.load()
320320

321321
@property
322-
def size(self):
322+
def size(self) -> tuple[int, int]:
323323
return self._size
324324

325325
@size.setter
326-
def size(self, value):
326+
def size(self, value: tuple[int, int]) -> None:
327327
if value not in self.info["sizes"]:
328328
msg = "This is not one of the allowed sizes of this image"
329329
raise ValueError(msg)

src/PIL/Image.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ def __truediv__(self, other: _E | float) -> _E:
504504
return _E(self.scale / other, self.offset / other)
505505

506506

507-
def _getscaleoffset(expr) -> tuple[float, float]:
507+
def _getscaleoffset(expr: Callable[[_E], _E | float]) -> tuple[float, float]:
508508
a = expr(_E(1, 0))
509509
return (a.scale, a.offset) if isinstance(a, _E) else (0, a)
510510

@@ -1894,7 +1894,13 @@ def alpha_composite(
18941894

18951895
def point(
18961896
self,
1897-
lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler,
1897+
lut: (
1898+
Sequence[float]
1899+
| NumpyArray
1900+
| Callable[[int], float]
1901+
| Callable[[_E], _E | float]
1902+
| ImagePointHandler
1903+
),
18981904
mode: str | None = None,
18991905
) -> Image:
19001906
"""
@@ -1930,10 +1936,10 @@ def point(self, data):
19301936
# check if the function can be used with point_transform
19311937
# UNDONE wiredfool -- I think this prevents us from ever doing
19321938
# a gamma function point transform on > 8bit images.
1933-
scale, offset = _getscaleoffset(lut)
1939+
scale, offset = _getscaleoffset(lut) # type: ignore[arg-type]
19341940
return self._new(self.im.point_transform(scale, offset))
19351941
# for other modes, convert the function to a table
1936-
flatLut = [lut(i) for i in range(256)] * self.im.bands
1942+
flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type]
19371943
else:
19381944
flatLut = lut
19391945

@@ -2869,11 +2875,11 @@ def __transformer(
28692875
self,
28702876
box: tuple[int, int, int, int],
28712877
image: Image,
2872-
method,
2873-
data,
2878+
method: Transform,
2879+
data: Sequence[float],
28742880
resample: int = Resampling.NEAREST,
28752881
fill: bool = True,
2876-
):
2882+
) -> None:
28772883
w = box[2] - box[0]
28782884
h = box[3] - box[1]
28792885

@@ -4008,15 +4014,19 @@ def tobytes(self, offset: int = 8) -> bytes:
40084014
ifd[tag] = value
40094015
return b"Exif\x00\x00" + head + ifd.tobytes(offset)
40104016

4011-
def get_ifd(self, tag):
4017+
def get_ifd(self, tag: int) -> dict[int, Any]:
40124018
if tag not in self._ifds:
40134019
if tag == ExifTags.IFD.IFD1:
40144020
if self._info is not None and self._info.next != 0:
4015-
self._ifds[tag] = self._get_ifd_dict(self._info.next)
4021+
ifd = self._get_ifd_dict(self._info.next)
4022+
if ifd is not None:
4023+
self._ifds[tag] = ifd
40164024
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
40174025
offset = self._hidden_data.get(tag, self.get(tag))
40184026
if offset is not None:
4019-
self._ifds[tag] = self._get_ifd_dict(offset, tag)
4027+
ifd = self._get_ifd_dict(offset, tag)
4028+
if ifd is not None:
4029+
self._ifds[tag] = ifd
40204030
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
40214031
if ExifTags.IFD.Exif not in self._ifds:
40224032
self.get_ifd(ExifTags.IFD.Exif)
@@ -4073,7 +4083,9 @@ def get_ifd(self, tag):
40734083
(offset,) = struct.unpack(">L", data)
40744084
self.fp.seek(offset)
40754085

4076-
camerainfo = {"ModelID": self.fp.read(4)}
4086+
camerainfo: dict[str, int | bytes] = {
4087+
"ModelID": self.fp.read(4)
4088+
}
40774089

40784090
self.fp.read(4)
40794091
# Seconds since 2000
@@ -4089,16 +4101,18 @@ def get_ifd(self, tag):
40894101
][1]
40904102
camerainfo["Parallax"] = handler(
40914103
ImageFileDirectory_v2(), parallax, False
4092-
)
4104+
)[0]
40934105

40944106
self.fp.read(4)
40954107
camerainfo["Category"] = self.fp.read(2)
40964108

4097-
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
4109+
makernote = {0x1101: camerainfo}
40984110
self._ifds[tag] = makernote
40994111
else:
41004112
# Interop
4101-
self._ifds[tag] = self._get_ifd_dict(tag_data, tag)
4113+
ifd = self._get_ifd_dict(tag_data, tag)
4114+
if ifd is not None:
4115+
self._ifds[tag] = ifd
41024116
ifd = self._ifds.get(tag, {})
41034117
if tag == ExifTags.IFD.Exif and self._hidden_data:
41044118
ifd = {

src/PIL/ImageFile.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import abc
3232
import io
3333
import itertools
34+
import os
3435
import struct
3536
import sys
3637
from typing import IO, Any, NamedTuple
@@ -555,7 +556,7 @@ def _encode_tile(
555556
fp: IO[bytes],
556557
tile: list[_Tile],
557558
bufsize: int,
558-
fh,
559+
fh: int | None,
559560
exc: BaseException | None = None,
560561
) -> None:
561562
for encoder_name, extents, offset, args in tile:
@@ -577,6 +578,7 @@ def _encode_tile(
577578
break
578579
else:
579580
# slight speedup: compress to real file object
581+
assert fh is not None
580582
errcode = encoder.encode_to_file(fh, bufsize)
581583
if errcode < 0:
582584
raise _get_oserror(errcode, encoder=True) from exc
@@ -801,7 +803,7 @@ def encode_to_pyfd(self) -> tuple[int, int]:
801803
self.fd.write(data)
802804
return bytes_consumed, errcode
803805

804-
def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
806+
def encode_to_file(self, fh: int, bufsize: int) -> int:
805807
"""
806808
:param fh: File handle.
807809
:param bufsize: Buffer size.
@@ -814,5 +816,5 @@ def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
814816
while errcode == 0:
815817
status, errcode, buf = self.encode(bufsize)
816818
if status > 0:
817-
fh.write(buf[status:])
819+
os.write(fh, buf[status:])
818820
return errcode

src/PIL/TiffImagePlugin.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -624,12 +624,12 @@ def reset(self) -> None:
624624
self._tagdata: dict[int, bytes] = {}
625625
self.tagtype = {} # added 2008-06-05 by Florian Hoech
626626
self._next = None
627-
self._offset = None
627+
self._offset: int | None = None
628628

629629
def __str__(self) -> str:
630630
return str(dict(self))
631631

632-
def named(self):
632+
def named(self) -> dict[str, Any]:
633633
"""
634634
:returns: dict of name|key: value
635635
@@ -643,7 +643,7 @@ def named(self):
643643
def __len__(self) -> int:
644644
return len(set(self._tagdata) | set(self._tags_v2))
645645

646-
def __getitem__(self, tag):
646+
def __getitem__(self, tag: int) -> Any:
647647
if tag not in self._tags_v2: # unpack on the fly
648648
data = self._tagdata[tag]
649649
typ = self.tagtype[tag]
@@ -855,7 +855,7 @@ def _ensure_read(self, fp: IO[bytes], size: int) -> bytes:
855855
raise OSError(msg)
856856
return ret
857857

858-
def load(self, fp):
858+
def load(self, fp: IO[bytes]) -> None:
859859
self.reset()
860860
self._offset = fp.tell()
861861

@@ -1098,7 +1098,7 @@ def __setitem__(self, tag: int, value: Any) -> None:
10981098
for legacy_api in (False, True):
10991099
self._setitem(tag, value, legacy_api)
11001100

1101-
def __getitem__(self, tag):
1101+
def __getitem__(self, tag: int) -> Any:
11021102
if tag not in self._tags_v1: # unpack on the fly
11031103
data = self._tagdata[tag]
11041104
typ = self.tagtype[tag]
@@ -1124,11 +1124,8 @@ class TiffImageFile(ImageFile.ImageFile):
11241124
format_description = "Adobe TIFF"
11251125
_close_exclusive_fp_after_loading = False
11261126

1127-
def __init__(self, fp=None, filename=None):
1128-
self.tag_v2 = None
1129-
""" Image file directory (tag dictionary) """
1130-
1131-
self.tag = None
1127+
def __init__(self, fp=None, filename=None) -> None:
1128+
self.tag: ImageFileDirectory_v1 | None = None
11321129
""" Legacy tag entries """
11331130

11341131
super().__init__(fp, filename)
@@ -1396,8 +1393,11 @@ def _setup(self) -> None:
13961393
logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
13971394

13981395
# size
1399-
xsize = int(self.tag_v2.get(IMAGEWIDTH))
1400-
ysize = int(self.tag_v2.get(IMAGELENGTH))
1396+
xsize = self.tag_v2.get(IMAGEWIDTH)
1397+
ysize = self.tag_v2.get(IMAGELENGTH)
1398+
if not isinstance(xsize, int) or not isinstance(ysize, int):
1399+
msg = "Invalid dimensions"
1400+
raise ValueError(msg)
14011401
self._size = xsize, ysize
14021402

14031403
logger.debug("- size: %s", self.size)
@@ -1545,8 +1545,12 @@ def _setup(self) -> None:
15451545
else:
15461546
# tiled image
15471547
offsets = self.tag_v2[TILEOFFSETS]
1548-
w = self.tag_v2.get(TILEWIDTH)
1548+
tilewidth = self.tag_v2.get(TILEWIDTH)
15491549
h = self.tag_v2.get(TILELENGTH)
1550+
if not isinstance(tilewidth, int) or not isinstance(h, int):
1551+
msg = "Invalid tile dimensions"
1552+
raise ValueError(msg)
1553+
w = tilewidth
15501554

15511555
for offset in offsets:
15521556
if x + w > xsize:
@@ -1624,7 +1628,7 @@ def _setup(self) -> None:
16241628
}
16251629

16261630

1627-
def _save(im, fp, filename):
1631+
def _save(im: Image.Image, fp, filename: str | bytes) -> None:
16281632
try:
16291633
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
16301634
except KeyError as e:
@@ -1760,10 +1764,11 @@ def _save(im, fp, filename):
17601764
if im.mode == "1":
17611765
inverted_im = im.copy()
17621766
px = inverted_im.load()
1763-
for y in range(inverted_im.height):
1764-
for x in range(inverted_im.width):
1765-
px[x, y] = 0 if px[x, y] == 255 else 255
1766-
im = inverted_im
1767+
if px is not None:
1768+
for y in range(inverted_im.height):
1769+
for x in range(inverted_im.width):
1770+
px[x, y] = 0 if px[x, y] == 255 else 255
1771+
im = inverted_im
17671772
else:
17681773
im = ImageOps.invert(im)
17691774

@@ -1805,11 +1810,11 @@ def _save(im, fp, filename):
18051810
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
18061811

18071812
if im.mode == "YCbCr":
1808-
for tag, value in {
1813+
for tag, default_value in {
18091814
YCBCRSUBSAMPLING: (1, 1),
18101815
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
18111816
}.items():
1812-
ifd.setdefault(tag, value)
1817+
ifd.setdefault(tag, default_value)
18131818

18141819
blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
18151820
if libtiff:
@@ -1852,7 +1857,7 @@ def _save(im, fp, filename):
18521857
]
18531858

18541859
# bits per sample is a single short in the tiff directory, not a list.
1855-
atts = {BITSPERSAMPLE: bits[0]}
1860+
atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}
18561861
# Merge the ones that we have with (optional) more bits from
18571862
# the original file, e.g x,y resolution so that we can
18581863
# save(load('')) == original file.
@@ -1923,13 +1928,15 @@ def _save(im, fp, filename):
19231928
offset = ifd.save(fp)
19241929

19251930
ImageFile._save(
1926-
im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))]
1931+
im,
1932+
fp,
1933+
[ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))],
19271934
)
19281935

19291936
# -- helper for multi-page save --
19301937
if "_debug_multipage" in encoderinfo:
19311938
# just to access o32 and o16 (using correct byte order)
1932-
im._debug_multipage = ifd
1939+
setattr(im, "_debug_multipage", ifd)
19331940

19341941

19351942
class AppendingTiffWriter:

0 commit comments

Comments
 (0)