Added type hints

This commit is contained in:
Andrew Murray 2024-07-30 20:20:09 +10:00
parent 5833a8b18e
commit accfaf1c09
9 changed files with 101 additions and 53 deletions

View File

@ -154,7 +154,7 @@ class TestFileJpeg:
assert k > 0.9 assert k > 0.9
def test_rgb(self) -> None: def test_rgb(self) -> None:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]:
return tuple(v[0] for v in im.layer) return tuple(v[0] for v in im.layer)
im = hopper() im = hopper()

View File

@ -99,7 +99,7 @@ class TestImage:
im = Image.new("L", (100, 100)) im = Image.new("L", (100, 100))
p = Pretty() p = Pretty()
im._repr_pretty_(p, None) im._repr_pretty_(p, False)
assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>" assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>"
def test_open_formats(self) -> None: def test_open_formats(self) -> None:

View File

@ -185,6 +185,14 @@ Plugin reference
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL.MpoImagePlugin` Module
----------------------------------
.. automodule:: PIL.MpoImagePlugin
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.MspImagePlugin` Module :mod:`~PIL.MspImagePlugin` Module
--------------------------------- ---------------------------------

View File

@ -65,16 +65,24 @@ def has_ghostscript() -> bool:
return gs_binary is not False return gs_binary is not False
def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image: def Ghostscript(
tile: list[ImageFile._Tile],
size: tuple[int, int],
fp: IO[bytes],
scale: int = 1,
transparency: bool = False,
) -> Image.Image:
"""Render an image using Ghostscript""" """Render an image using Ghostscript"""
global gs_binary global gs_binary
if not has_ghostscript(): if not has_ghostscript():
msg = "Unable to locate Ghostscript on paths" msg = "Unable to locate Ghostscript on paths"
raise OSError(msg) raise OSError(msg)
assert isinstance(gs_binary, str)
# Unpack decoder tile # Unpack decoder tile
decoder, tile, offset, data = tile[0] args = tile[0].args
length, bbox = data assert isinstance(args, tuple)
length, bbox = args
# Hack to support hi-res rendering # Hack to support hi-res rendering
scale = int(scale) or 1 scale = int(scale) or 1
@ -227,7 +235,11 @@ class EpsImageFile(ImageFile.ImageFile):
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(i)) for i in v.split()] box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1] self._size = box[2] - box[0], box[3] - box[1]
self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] self.tile = [
ImageFile._Tile(
"eps", (0, 0) + self.size, offset, (length, box)
)
]
except Exception: except Exception:
pass pass
return True return True

View File

@ -220,7 +220,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
if TYPE_CHECKING: if TYPE_CHECKING:
from xml.etree.ElementTree import Element from xml.etree.ElementTree import Element
from . import ImageFile, ImagePalette from . import ImageFile, ImagePalette, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
@ -676,7 +676,7 @@ class Image:
id(self), id(self),
) )
def _repr_pretty_(self, p, cycle) -> None: def _repr_pretty_(self, p, cycle: bool) -> None:
"""IPython plain text display support""" """IPython plain text display support"""
# Same as __repr__ but without unpredictable id(self), # Same as __repr__ but without unpredictable id(self),
@ -1551,6 +1551,7 @@ class Image:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(513): if ifd1 and ifd1.get(513):
assert exif._info is not None
ifds.append((ifd1, exif._info.next)) ifds.append((ifd1, exif._info.next))
offset = None offset = None
@ -1560,12 +1561,13 @@ class Image:
offset = current_offset offset = current_offset
fp = self.fp fp = self.fp
thumbnail_offset = ifd.get(513) if ifd is not None:
if thumbnail_offset is not None: thumbnail_offset = ifd.get(513)
thumbnail_offset += getattr(self, "_exif_offset", 0) if thumbnail_offset is not None:
self.fp.seek(thumbnail_offset) thumbnail_offset += getattr(self, "_exif_offset", 0)
data = self.fp.read(ifd.get(514)) self.fp.seek(thumbnail_offset)
fp = io.BytesIO(data) data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data)
with open(fp) as im: with open(fp) as im:
from . import TiffImagePlugin from . import TiffImagePlugin
@ -3869,14 +3871,14 @@ class Exif(_ExifBase):
bigtiff = False bigtiff = False
_loaded = False _loaded = False
def __init__(self): def __init__(self) -> None:
self._data = {} self._data: dict[int, Any] = {}
self._hidden_data = {} self._hidden_data: dict[int, Any] = {}
self._ifds = {} self._ifds: dict[int, dict[int, Any]] = {}
self._info = None self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None
self._loaded_exif = None self._loaded_exif: bytes | None = None
def _fixup(self, value): def _fixup(self, value: Any) -> Any:
try: try:
if len(value) == 1 and isinstance(value, tuple): if len(value) == 1 and isinstance(value, tuple):
return value[0] return value[0]
@ -3884,24 +3886,26 @@ class Exif(_ExifBase):
pass pass
return value return value
def _fixup_dict(self, src_dict): def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]:
# Helper function # Helper function
# returns a dict with any single item tuples/lists as individual values # returns a dict with any single item tuples/lists as individual values
return {k: self._fixup(v) for k, v in src_dict.items()} return {k: self._fixup(v) for k, v in src_dict.items()}
def _get_ifd_dict(self, offset: int, group: int | None = None): def _get_ifd_dict(
self, offset: int, group: int | None = None
) -> dict[int, Any] | None:
try: try:
# an offset pointer to the location of the nested embedded IFD. # an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted. # It should be a long, but may be corrupted.
self.fp.seek(offset) self.fp.seek(offset)
except (KeyError, TypeError): except (KeyError, TypeError):
pass return None
else: else:
from . import TiffImagePlugin from . import TiffImagePlugin
info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group)
info.load(self.fp) info.load(self.fp)
return self._fixup_dict(info) return self._fixup_dict(dict(info))
def _get_head(self) -> bytes: def _get_head(self) -> bytes:
version = b"\x2B" if self.bigtiff else b"\x2A" version = b"\x2B" if self.bigtiff else b"\x2A"
@ -3966,7 +3970,7 @@ class Exif(_ExifBase):
self.fp.seek(offset) self.fp.seek(offset)
self._info.load(self.fp) self._info.load(self.fp)
def _get_merged_dict(self): def _get_merged_dict(self) -> dict[int, Any]:
merged_dict = dict(self) merged_dict = dict(self)
# get EXIF extension # get EXIF extension
@ -4124,7 +4128,7 @@ class Exif(_ExifBase):
keys.update(self._info) keys.update(self._info)
return len(keys) return len(keys)
def __getitem__(self, tag: int): def __getitem__(self, tag: int) -> Any:
if self._info is not None and tag not in self._data and tag in self._info: if self._info is not None and tag not in self._data and tag in self._info:
self._data[tag] = self._fixup(self._info[tag]) self._data[tag] = self._fixup(self._info[tag])
del self._info[tag] del self._info[tag]
@ -4133,7 +4137,7 @@ class Exif(_ExifBase):
def __contains__(self, tag: object) -> bool: def __contains__(self, tag: object) -> bool:
return tag in self._data or (self._info is not None and tag in self._info) return tag in self._data or (self._info is not None and tag in self._info)
def __setitem__(self, tag: int, value) -> None: def __setitem__(self, tag: int, value: Any) -> None:
if self._info is not None and tag in self._info: if self._info is not None and tag in self._info:
del self._info[tag] del self._info[tag]
self._data[tag] = value self._data[tag] = value

View File

@ -86,7 +86,7 @@ def raise_oserror(error: int) -> OSError:
raise _get_oserror(error, encoder=False) raise _get_oserror(error, encoder=False)
def _tilesort(t) -> int: def _tilesort(t: _Tile) -> int:
# sort on offset # sort on offset
return t[2] return t[2]
@ -161,7 +161,7 @@ class ImageFile(Image.Image):
return Image.MIME.get(self.format.upper()) return Image.MIME.get(self.format.upper())
return None return None
def __setstate__(self, state) -> None: def __setstate__(self, state: list[Any]) -> None:
self.tile = [] self.tile = []
super().__setstate__(state) super().__setstate__(state)
@ -525,7 +525,7 @@ class Parser:
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im, fp, tile, bufsize: int = 0) -> None: def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None:
"""Helper to save image based on tile list """Helper to save image based on tile list
:param im: Image object. :param im: Image object.
@ -554,7 +554,12 @@ def _save(im, fp, tile, bufsize: int = 0) -> None:
def _encode_tile( def _encode_tile(
im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None im: Image.Image,
fp: IO[bytes],
tile: list[_Tile],
bufsize: int,
fh,
exc: BaseException | None = None,
) -> None: ) -> None:
for encoder_name, extents, offset, args in tile: for encoder_name, extents, offset, args in tile:
if offset > 0: if offset > 0:
@ -664,7 +669,11 @@ class PyCodec:
""" """
self.fd = fd self.fd = fd
def setimage(self, im, extents=None): def setimage(
self,
im: Image.core.ImagingCore,
extents: tuple[int, int, int, int] | None = None,
) -> None:
""" """
Called from ImageFile to set the core output image for the codec Called from ImageFile to set the core output image for the codec

View File

@ -42,7 +42,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import warnings import warnings
from typing import IO, Any from typing import IO, TYPE_CHECKING, Any
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -51,6 +51,9 @@ from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from .JpegPresets import presets from .JpegPresets import presets
if TYPE_CHECKING:
from .MpoImagePlugin import MpoImageFile
# #
# Parser # Parser
@ -329,7 +332,7 @@ class JpegImageFile(ImageFile.ImageFile):
format = "JPEG" format = "JPEG"
format_description = "JPEG (ISO 10918)" format_description = "JPEG (ISO 10918)"
def _open(self): def _open(self) -> None:
s = self.fp.read(3) s = self.fp.read(3)
if not _accept(s): if not _accept(s):
@ -342,13 +345,13 @@ class JpegImageFile(ImageFile.ImageFile):
self._exif_offset = 0 self._exif_offset = 0
# JPEG specifics (internal) # JPEG specifics (internal)
self.layer = [] self.layer: list[tuple[int, int, int, int]] = []
self.huffman_dc = {} self.huffman_dc: dict[Any, Any] = {}
self.huffman_ac = {} self.huffman_ac: dict[Any, Any] = {}
self.quantization = {} self.quantization: dict[int, list[int]] = {}
self.app = {} # compatibility self.app: dict[str, bytes] = {} # compatibility
self.applist = [] self.applist: list[tuple[str, bytes]] = []
self.icclist = [] self.icclist: list[bytes] = []
while True: while True:
i = s[0] i = s[0]
@ -831,7 +834,9 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
## ##
# Factory for making JPEG and MPO instances # Factory for making JPEG and MPO instances
def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None): def jpeg_factory(
fp: IO[bytes] | None = None, filename: str | bytes | None = None
) -> JpegImageFile | MpoImageFile:
im = JpegImageFile(fp, filename) im = JpegImageFile(fp, filename)
try: try:
mpheader = im._getmp() mpheader = im._getmp()

View File

@ -40,7 +40,7 @@ import warnings
import zlib import zlib
from collections.abc import Callable from collections.abc import Callable
from enum import IntEnum from enum import IntEnum
from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -1223,7 +1223,11 @@ def _write_multiple_frames(
if default_image: if default_image:
if im.mode != mode: if im.mode != mode:
im = im.convert(mode) im = im.convert(mode)
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) ImageFile._save(
im,
cast(IO[bytes], _idat(fp, chunk)),
[("zip", (0, 0) + im.size, 0, rawmode)],
)
seq_num = 0 seq_num = 0
for frame, frame_data in enumerate(im_frames): for frame, frame_data in enumerate(im_frames):
@ -1258,14 +1262,14 @@ def _write_multiple_frames(
# first frame must be in IDAT chunks for backwards compatibility # first frame must be in IDAT chunks for backwards compatibility
ImageFile._save( ImageFile._save(
im_frame, im_frame,
_idat(fp, chunk), cast(IO[bytes], _idat(fp, chunk)),
[("zip", (0, 0) + im_frame.size, 0, rawmode)], [("zip", (0, 0) + im_frame.size, 0, rawmode)],
) )
else: else:
fdat_chunks = _fdat(fp, chunk, seq_num) fdat_chunks = _fdat(fp, chunk, seq_num)
ImageFile._save( ImageFile._save(
im_frame, im_frame,
fdat_chunks, cast(IO[bytes], fdat_chunks),
[("zip", (0, 0) + im_frame.size, 0, rawmode)], [("zip", (0, 0) + im_frame.size, 0, rawmode)],
) )
seq_num = fdat_chunks.seq_num seq_num = fdat_chunks.seq_num
@ -1465,7 +1469,9 @@ def _save(
) )
if single_im: if single_im:
ImageFile._save( ImageFile._save(
single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)] single_im,
cast(IO[bytes], _idat(fp, chunk)),
[("zip", (0, 0) + single_im.size, 0, rawmode)],
) )
if info: if info:

View File

@ -288,8 +288,10 @@ def _accept(prefix: bytes) -> bool:
return prefix[:4] in PREFIXES return prefix[:4] in PREFIXES
def _limit_rational(val, max_val): def _limit_rational(
inv = abs(val) > 1 val: float | Fraction | IFDRational, max_val: int
) -> tuple[float, float]:
inv = abs(float(val)) > 1
n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
return n_d[::-1] if inv else n_d return n_d[::-1] if inv else n_d
@ -792,7 +794,9 @@ class ImageFileDirectory_v2(_IFDv2Base):
return value + b"\0" return value + b"\0"
@_register_loader(5, 8) @_register_loader(5, 8)
def load_rational(self, data, legacy_api: bool = True): def load_rational(
self, data: bytes, legacy_api: bool = True
) -> tuple[tuple[int, int] | IFDRational, ...]:
vals = self._unpack(f"{len(data) // 4}L", data) vals = self._unpack(f"{len(data) // 4}L", data)
def combine(a: int, b: int) -> tuple[int, int] | IFDRational: def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
@ -801,7 +805,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
@_register_writer(5) @_register_writer(5)
def write_rational(self, *values) -> bytes: def write_rational(self, *values: IFDRational) -> bytes:
return b"".join( return b"".join(
self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
) )
@ -828,7 +832,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
@_register_writer(10) @_register_writer(10)
def write_signed_rational(self, *values) -> bytes: def write_signed_rational(self, *values: IFDRational) -> bytes:
return b"".join( return b"".join(
self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
for frac in values for frac in values