Skip to content

Commit

Permalink
Added type hints
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Aug 20, 2024
1 parent 2ed8502 commit dab3346
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 45 deletions.
7 changes: 7 additions & 0 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,13 @@ def test_planar_configuration_save(self, tmp_path: Path) -> None:
with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile)

def test_invalid_tiled_dimensions(self) -> None:
with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
data = fp.read()
b = BytesIO(data[:144] + b"\x02" + data[145:])
with pytest.raises(ValueError):
Image.open(b)

@pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
Expand Down
3 changes: 1 addition & 2 deletions Tests/test_imagefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,8 @@ def test_encode(self) -> None:
with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd()

fh = BytesIO()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(fh, 0)
encoder.encode_to_file(0, 0)

def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError):
Expand Down
1 change: 1 addition & 0 deletions docs/reference/Image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ Classes
:show-inheritance:
.. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImageTransformHandler
.. autoclass:: PIL.Image._E

Protocols
---------
Expand Down
4 changes: 2 additions & 2 deletions src/PIL/IcoImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,11 @@ def _open(self) -> None:
self.load()

@property
def size(self):
def size(self) -> tuple[int, int]:
return self._size

@size.setter
def size(self, value):
def size(self, value: tuple[int, int]) -> None:
if value not in self.info["sizes"]:
msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg)
Expand Down
42 changes: 28 additions & 14 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ def __truediv__(self, other: _E | float) -> _E:
return _E(self.scale / other, self.offset / other)


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

Expand Down Expand Up @@ -1894,7 +1894,13 @@ def alpha_composite(

def point(
self,
lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler,
lut: (
Sequence[float]
| NumpyArray
| Callable[[int], float]
| Callable[[_E], _E | float]
| ImagePointHandler
),
mode: str | None = None,
) -> Image:
"""
Expand Down Expand Up @@ -1930,10 +1936,10 @@ def point(self, data):
# check if the function can be used with point_transform
# UNDONE wiredfool -- I think this prevents us from ever doing
# a gamma function point transform on > 8bit images.
scale, offset = _getscaleoffset(lut)
scale, offset = _getscaleoffset(lut) # type: ignore[arg-type]
return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table
flatLut = [lut(i) for i in range(256)] * self.im.bands
flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type]
else:
flatLut = lut

Expand Down Expand Up @@ -2869,11 +2875,11 @@ def __transformer(
self,
box: tuple[int, int, int, int],
image: Image,
method,
data,
method: Transform,
data: Sequence[float],
resample: int = Resampling.NEAREST,
fill: bool = True,
):
) -> None:
w = box[2] - box[0]
h = box[3] - box[1]

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

def get_ifd(self, tag):
def get_ifd(self, tag: int) -> dict[int, Any]:
if tag not in self._ifds:
if tag == ExifTags.IFD.IFD1:
if self._info is not None and self._info.next != 0:
self._ifds[tag] = self._get_ifd_dict(self._info.next)
ifd = self._get_ifd_dict(self._info.next)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
offset = self._hidden_data.get(tag, self.get(tag))
if offset is not None:
self._ifds[tag] = self._get_ifd_dict(offset, tag)
ifd = self._get_ifd_dict(offset, tag)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif)
Expand Down Expand Up @@ -4073,7 +4083,9 @@ def get_ifd(self, tag):
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)

camerainfo = {"ModelID": self.fp.read(4)}
camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}

self.fp.read(4)
# Seconds since 2000
Expand All @@ -4089,16 +4101,18 @@ def get_ifd(self, tag):
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)
)[0]

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

makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote
else:
# Interop
self._ifds[tag] = self._get_ifd_dict(tag_data, tag)
ifd = self._get_ifd_dict(tag_data, tag)
if ifd is not None:
self._ifds[tag] = ifd
ifd = self._ifds.get(tag, {})
if tag == ExifTags.IFD.Exif and self._hidden_data:
ifd = {
Expand Down
8 changes: 5 additions & 3 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import abc
import io
import itertools
import os
import struct
import sys
from typing import IO, Any, NamedTuple
Expand Down Expand Up @@ -555,7 +556,7 @@ def _encode_tile(
fp: IO[bytes],
tile: list[_Tile],
bufsize: int,
fh,
fh: int | None,
exc: BaseException | None = None,
) -> None:
for encoder_name, extents, offset, args in tile:
Expand All @@ -577,6 +578,7 @@ def _encode_tile(
break
else:
# slight speedup: compress to real file object
assert fh is not None
errcode = encoder.encode_to_file(fh, bufsize)
if errcode < 0:
raise _get_oserror(errcode, encoder=True) from exc
Expand Down Expand Up @@ -801,7 +803,7 @@ def encode_to_pyfd(self) -> tuple[int, int]:
self.fd.write(data)
return bytes_consumed, errcode

def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
def encode_to_file(self, fh: int, bufsize: int) -> int:
"""
:param fh: File handle.
:param bufsize: Buffer size.
Expand All @@ -814,5 +816,5 @@ def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
while errcode == 0:
status, errcode, buf = self.encode(bufsize)
if status > 0:
fh.write(buf[status:])
os.write(fh, buf[status:])

Check warning on line 819 in src/PIL/ImageFile.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageFile.py#L819

Added line #L819 was not covered by tests
return errcode
55 changes: 31 additions & 24 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,12 +624,12 @@ def reset(self) -> None:
self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None
self._offset = None
self._offset: int | None = None

def __str__(self) -> str:
return str(dict(self))

def named(self):
def named(self) -> dict[str, Any]:
"""
:returns: dict of name|key: value
Expand All @@ -643,7 +643,7 @@ def named(self):
def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v2))

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

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

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

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

def __init__(self, fp=None, filename=None):
self.tag_v2 = None
def __init__(self, fp=None, filename=None) -> None:
self.tag_v2: ImageFileDirectory_v2
""" Image file directory (tag dictionary) """

self.tag = None
self.tag: ImageFileDirectory_v1
""" Legacy tag entries """

super().__init__(fp, filename)
Expand All @@ -1143,9 +1143,6 @@ def _open(self) -> None:

self.tag_v2 = ImageFileDirectory_v2(ifh)

# legacy IFD entries will be filled in later
self.ifd: ImageFileDirectory_v1 | None = None

# setup frame pointers
self.__first = self.__next = self.tag_v2.next
self.__frame = -1
Expand Down Expand Up @@ -1396,8 +1393,11 @@ def _setup(self) -> None:
logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))

# size
xsize = int(self.tag_v2.get(IMAGEWIDTH))
ysize = int(self.tag_v2.get(IMAGELENGTH))
xsize = self.tag_v2.get(IMAGEWIDTH)
ysize = self.tag_v2.get(IMAGELENGTH)
if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions"
raise ValueError(msg)

Check warning on line 1400 in src/PIL/TiffImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/TiffImagePlugin.py#L1399-L1400

Added lines #L1399 - L1400 were not covered by tests
self._size = xsize, ysize

logger.debug("- size: %s", self.size)
Expand Down Expand Up @@ -1545,8 +1545,12 @@ def _setup(self) -> None:
else:
# tiled image
offsets = self.tag_v2[TILEOFFSETS]
w = self.tag_v2.get(TILEWIDTH)
tilewidth = self.tag_v2.get(TILEWIDTH)
h = self.tag_v2.get(TILELENGTH)
if not isinstance(tilewidth, int) or not isinstance(h, int):
msg = "Invalid tile dimensions"
raise ValueError(msg)
w = tilewidth

for offset in offsets:
if x + w > xsize:
Expand Down Expand Up @@ -1624,7 +1628,7 @@ def _setup(self) -> None:
}


def _save(im, fp, filename):
def _save(im: Image.Image, fp, filename: str | bytes) -> None:
try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e:
Expand Down Expand Up @@ -1760,10 +1764,11 @@ def _save(im, fp, filename):
if im.mode == "1":
inverted_im = im.copy()
px = inverted_im.load()
for y in range(inverted_im.height):
for x in range(inverted_im.width):
px[x, y] = 0 if px[x, y] == 255 else 255
im = inverted_im
if px is not None:
for y in range(inverted_im.height):
for x in range(inverted_im.width):
px[x, y] = 0 if px[x, y] == 255 else 255
im = inverted_im
else:
im = ImageOps.invert(im)

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

if im.mode == "YCbCr":
for tag, value in {
for tag, default_value in {
YCBCRSUBSAMPLING: (1, 1),
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
}.items():
ifd.setdefault(tag, value)
ifd.setdefault(tag, default_value)

blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
if libtiff:
Expand Down Expand Up @@ -1852,7 +1857,7 @@ def _save(im, fp, filename):
]

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

ImageFile._save(
im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))]
im,
fp,
[ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))],
)

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

Check warning on line 1939 in src/PIL/TiffImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/TiffImagePlugin.py#L1939

Added line #L1939 was not covered by tests


class AppendingTiffWriter:
Expand Down

0 comments on commit dab3346

Please sign in to comment.