Added type hints

This commit is contained in:
Andrew Murray 2024-08-21 08:05:02 +10:00
parent 2ed8502d12
commit e6e5ef5c5f
8 changed files with 80 additions and 46 deletions

View File

@ -684,6 +684,13 @@ class TestFileTiff:
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")

View File

@ -412,9 +412,8 @@ class TestPyEncoder(CodecsTest):
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):

View File

@ -363,6 +363,7 @@ Classes
:show-inheritance:
.. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImageTransformHandler
.. autoclass:: PIL.Image._E
Protocols
---------

View File

@ -319,11 +319,11 @@ class IcoImageFile(ImageFile.ImageFile):
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)

View File

@ -504,7 +504,7 @@ class _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)
@ -1894,7 +1894,13 @@ class Image:
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:
"""
@ -1930,10 +1936,10 @@ class Image:
# 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
@ -2869,11 +2875,11 @@ class Image:
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]
@ -4008,15 +4014,19 @@ class Exif(_ExifBase):
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)
@ -4073,7 +4083,9 @@ class Exif(_ExifBase):
(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
@ -4089,16 +4101,18 @@ class Exif(_ExifBase):
][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 = {

View File

@ -31,6 +31,7 @@ from __future__ import annotations
import abc
import io
import itertools
import os
import struct
import sys
from typing import IO, Any, NamedTuple
@ -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:
@ -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
@ -801,7 +803,7 @@ class PyEncoder(PyCodec):
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.
@ -814,5 +816,5 @@ class PyEncoder(PyCodec):
while errcode == 0:
status, errcode, buf = self.encode(bufsize)
if status > 0:
fh.write(buf[status:])
os.write(fh, buf[status:])
return errcode

View File

@ -214,7 +214,7 @@ def getiptcinfo(
# as 4-byte integers, so we cannot use the get method...)
try:
data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK]
except (AttributeError, KeyError):
except KeyError:
pass
if data is None:

View File

@ -624,12 +624,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
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
@ -643,7 +643,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
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]
@ -855,7 +855,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise OSError(msg)
return ret
def load(self, fp):
def load(self, fp: IO[bytes]) -> None:
self.reset()
self._offset = fp.tell()
@ -1098,7 +1098,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
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]
@ -1124,11 +1124,15 @@ 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: StrOrBytesPath | IO[bytes] | None = None,
filename: str | bytes | None = 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)
@ -1143,9 +1147,6 @@ class TiffImageFile(ImageFile.ImageFile):
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
@ -1396,8 +1397,11 @@ class TiffImageFile(ImageFile.ImageFile):
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)
self._size = xsize, ysize
logger.debug("- size: %s", self.size)
@ -1545,8 +1549,12 @@ class TiffImageFile(ImageFile.ImageFile):
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:
@ -1624,7 +1632,7 @@ SAVE_INFO = {
}
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:
@ -1760,10 +1768,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)
@ -1805,11 +1814,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:
@ -1852,7 +1861,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.
@ -1923,13 +1932,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)
class AppendingTiffWriter: