Added type hints

This commit is contained in:
Andrew Murray 2024-07-20 13:14:18 +10:00
parent f8a9a18e7d
commit f624460321
6 changed files with 86 additions and 67 deletions

View File

@ -240,10 +240,11 @@ class TestFileLibTiff(LibTiffTestCase):
new_ifd = TiffImagePlugin.ImageFileDirectory_v2() new_ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, info in core_items.items(): for tag, info in core_items.items():
if info.length == 1: assert info.type is not None
new_ifd[tag] = values[info.type] if not info.length:
if info.length == 0:
new_ifd[tag] = tuple(values[info.type] for _ in range(3)) new_ifd[tag] = tuple(values[info.type] for _ in range(3))
elif info.length == 1:
new_ifd[tag] = values[info.type]
else: else:
new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) new_ifd[tag] = tuple(values[info.type] for _ in range(info.length))

View File

@ -37,6 +37,11 @@ Example: Parse an image
Classes Classes
------- -------
.. autoclass:: PIL.ImageFile._Tile()
:member-order: bysource
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.Parser() .. autoclass:: PIL.ImageFile.Parser()
:members: :members:

View File

@ -90,7 +90,7 @@ class Dib:
assert not isinstance(image, str) assert not isinstance(image, str)
self.paste(image) self.paste(image)
def expose(self, handle): def expose(self, handle: int | HDC | HWND) -> None:
""" """
Copy the bitmap contents to a device context. Copy the bitmap contents to a device context.
@ -101,19 +101,18 @@ class Dib:
if isinstance(handle, HWND): if isinstance(handle, HWND):
dc = self.image.getdc(handle) dc = self.image.getdc(handle)
try: try:
result = self.image.expose(dc) self.image.expose(dc)
finally: finally:
self.image.releasedc(handle, dc) self.image.releasedc(handle, dc)
else: else:
result = self.image.expose(handle) self.image.expose(handle)
return result
def draw( def draw(
self, self,
handle, handle: int | HDC | HWND,
dst: tuple[int, int, int, int], dst: tuple[int, int, int, int],
src: tuple[int, int, int, int] | None = None, src: tuple[int, int, int, int] | None = None,
): ) -> None:
""" """
Same as expose, but allows you to specify where to draw the image, and Same as expose, but allows you to specify where to draw the image, and
what part of it to draw. what part of it to draw.
@ -128,14 +127,13 @@ class Dib:
if isinstance(handle, HWND): if isinstance(handle, HWND):
dc = self.image.getdc(handle) dc = self.image.getdc(handle)
try: try:
result = self.image.draw(dc, dst, src) self.image.draw(dc, dst, src)
finally: finally:
self.image.releasedc(handle, dc) self.image.releasedc(handle, dc)
else: else:
result = self.image.draw(handle, dst, src) self.image.draw(handle, dst, src)
return result
def query_palette(self, handle): def query_palette(self, handle: int | HDC | HWND) -> int:
""" """
Installs the palette associated with the image in the given device Installs the palette associated with the image in the given device
context. context.
@ -147,8 +145,8 @@ class Dib:
:param handle: Device context (HDC), cast to a Python integer, or an :param handle: Device context (HDC), cast to a Python integer, or an
HDC or HWND instance. HDC or HWND instance.
:return: A true value if one or more entries were changed (this :return: The number of entries that were changed (if one or more entries,
indicates that the image should be redrawn). this indicates that the image should be redrawn).
""" """
if isinstance(handle, HWND): if isinstance(handle, HWND):
handle = self.image.getdc(handle) handle = self.image.getdc(handle)
@ -210,22 +208,22 @@ class Window:
title, self.__dispatcher, width or 0, height or 0 title, self.__dispatcher, width or 0, height or 0
) )
def __dispatcher(self, action: str, *args): def __dispatcher(self, action: str, *args: int) -> None:
return getattr(self, f"ui_handle_{action}")(*args) getattr(self, f"ui_handle_{action}")(*args)
def ui_handle_clear(self, dc, x0, y0, x1, y1) -> None: def ui_handle_clear(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
pass pass
def ui_handle_damage(self, x0, y0, x1, y1) -> None: def ui_handle_damage(self, x0: int, y0: int, x1: int, y1: int) -> None:
pass pass
def ui_handle_destroy(self) -> None: def ui_handle_destroy(self) -> None:
pass pass
def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None: def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
pass pass
def ui_handle_resize(self, width, height) -> None: def ui_handle_resize(self, width: int, height: int) -> None:
pass pass
def mainloop(self) -> None: def mainloop(self) -> None:
@ -235,12 +233,12 @@ class Window:
class ImageWindow(Window): class ImageWindow(Window):
"""Create an image window which displays the given image.""" """Create an image window which displays the given image."""
def __init__(self, image, title: str = "PIL") -> None: def __init__(self, image: Image.Image | Dib, title: str = "PIL") -> None:
if not isinstance(image, Dib): if not isinstance(image, Dib):
image = Dib(image) image = Dib(image)
self.image = image self.image = image
width, height = image.size width, height = image.size
super().__init__(title, width=width, height=height) super().__init__(title, width=width, height=height)
def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None: def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
self.image.draw(dc, (x0, y0, x1, y1)) self.image.draw(dc, (x0, y0, x1, y1))

View File

@ -19,6 +19,7 @@ from __future__ import annotations
import io import io
from functools import cached_property from functools import cached_property
from typing import IO
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i8 from ._binary import i8
@ -142,7 +143,9 @@ class PsdImageFile(ImageFile.ImageFile):
self._min_frame = 1 self._min_frame = 1
@cached_property @cached_property
def layers(self): def layers(
self,
) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
layers = [] layers = []
if self._layers_position is not None: if self._layers_position is not None:
self._fp.seek(self._layers_position) self._fp.seek(self._layers_position)
@ -181,7 +184,9 @@ class PsdImageFile(ImageFile.ImageFile):
return self.frame return self.frame
def _layerinfo(fp, ct_bytes): def _layerinfo(
fp: IO[bytes], ct_bytes: int
) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
# read layerinfo block # read layerinfo block
layers = [] layers = []
@ -203,7 +208,7 @@ def _layerinfo(fp, ct_bytes):
x1 = si32(read(4)) x1 = si32(read(4))
# image info # image info
mode = [] bands = []
ct_types = i16(read(2)) ct_types = i16(read(2))
if ct_types > 4: if ct_types > 4:
fp.seek(ct_types * 6 + 12, io.SEEK_CUR) fp.seek(ct_types * 6 + 12, io.SEEK_CUR)
@ -215,23 +220,23 @@ def _layerinfo(fp, ct_bytes):
type = i16(read(2)) type = i16(read(2))
if type == 65535: if type == 65535:
m = "A" b = "A"
else: else:
m = "RGBA"[type] b = "RGBA"[type]
mode.append(m) bands.append(b)
read(4) # size read(4) # size
# figure out the image mode # figure out the image mode
mode.sort() bands.sort()
if mode == ["R"]: if bands == ["R"]:
mode = "L" mode = "L"
elif mode == ["B", "G", "R"]: elif bands == ["B", "G", "R"]:
mode = "RGB" mode = "RGB"
elif mode == ["A", "B", "G", "R"]: elif bands == ["A", "B", "G", "R"]:
mode = "RGBA" mode = "RGBA"
else: else:
mode = None # unknown mode = "" # unknown
# skip over blend flags and extra information # skip over blend flags and extra information
read(12) # filler read(12) # filler
@ -258,19 +263,22 @@ def _layerinfo(fp, ct_bytes):
layers.append((name, mode, (x0, y0, x1, y1))) layers.append((name, mode, (x0, y0, x1, y1)))
# get tiles # get tiles
layerinfo = []
for i, (name, mode, bbox) in enumerate(layers): for i, (name, mode, bbox) in enumerate(layers):
tile = [] tile = []
for m in mode: for m in mode:
t = _maketile(fp, m, bbox, 1) t = _maketile(fp, m, bbox, 1)
if t: if t:
tile.extend(t) tile.extend(t)
layers[i] = name, mode, bbox, tile layerinfo.append((name, mode, bbox, tile))
return layers return layerinfo
def _maketile(file, mode, bbox, channels): def _maketile(
tile = None file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int
) -> list[ImageFile._Tile] | None:
tiles = None
read = file.read read = file.read
compression = i16(read(2)) compression = i16(read(2))
@ -283,26 +291,26 @@ def _maketile(file, mode, bbox, channels):
if compression == 0: if compression == 0:
# #
# raw compression # raw compression
tile = [] tiles = []
for channel in range(channels): for channel in range(channels):
layer = mode[channel] layer = mode[channel]
if mode == "CMYK": if mode == "CMYK":
layer += ";I" layer += ";I"
tile.append(("raw", bbox, offset, layer)) tiles.append(ImageFile._Tile("raw", bbox, offset, layer))
offset = offset + xsize * ysize offset = offset + xsize * ysize
elif compression == 1: elif compression == 1:
# #
# packbits compression # packbits compression
i = 0 i = 0
tile = [] tiles = []
bytecount = read(channels * ysize * 2) bytecount = read(channels * ysize * 2)
offset = file.tell() offset = file.tell()
for channel in range(channels): for channel in range(channels):
layer = mode[channel] layer = mode[channel]
if mode == "CMYK": if mode == "CMYK":
layer += ";I" layer += ";I"
tile.append(("packbits", bbox, offset, layer)) tiles.append(ImageFile._Tile("packbits", bbox, offset, layer))
for y in range(ysize): for y in range(ysize):
offset = offset + i16(bytecount, i) offset = offset + i16(bytecount, i)
i += 2 i += 2
@ -312,7 +320,7 @@ def _maketile(file, mode, bbox, channels):
if offset & 1: if offset & 1:
read(1) # padding read(1) # padding
return tile return tiles
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -445,7 +445,7 @@ class IFDRational(Rational):
__int__ = _delegate("__int__") __int__ = _delegate("__int__")
def _register_loader(idx, size): def _register_loader(idx: int, size: int):
def decorator(func): def decorator(func):
from .TiffTags import TYPES from .TiffTags import TYPES
@ -457,7 +457,7 @@ def _register_loader(idx, size):
return decorator return decorator
def _register_writer(idx): def _register_writer(idx: int):
def decorator(func): def decorator(func):
_write_dispatch[idx] = func # noqa: F821 _write_dispatch[idx] = func # noqa: F821
return func return func
@ -465,7 +465,7 @@ def _register_writer(idx):
return decorator return decorator
def _register_basic(idx_fmt_name): def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None:
from .TiffTags import TYPES from .TiffTags import TYPES
idx, fmt, name = idx_fmt_name idx, fmt, name = idx_fmt_name
@ -640,7 +640,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __contains__(self, tag: object) -> bool: def __contains__(self, tag: object) -> bool:
return tag in self._tags_v2 or tag in self._tagdata return tag in self._tags_v2 or tag in self._tagdata
def __setitem__(self, tag, value) -> None: def __setitem__(self, tag: int, value) -> None:
self._setitem(tag, value, self.legacy_api) self._setitem(tag, value, self.legacy_api)
def _setitem(self, tag, value, legacy_api) -> None: def _setitem(self, tag, value, legacy_api) -> None:
@ -731,10 +731,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __iter__(self): def __iter__(self):
return iter(set(self._tagdata) | set(self._tags_v2)) return iter(set(self._tagdata) | set(self._tags_v2))
def _unpack(self, fmt, data): def _unpack(self, fmt: str, data):
return struct.unpack(self._endian + fmt, data) return struct.unpack(self._endian + fmt, data)
def _pack(self, fmt, *values): def _pack(self, fmt: str, *values):
return struct.pack(self._endian + fmt, *values) return struct.pack(self._endian + fmt, *values)
list( list(
@ -755,7 +755,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
) )
@_register_loader(1, 1) # Basic type, except for the legacy API. @_register_loader(1, 1) # Basic type, except for the legacy API.
def load_byte(self, data, legacy_api=True): def load_byte(self, data, legacy_api: bool = True):
return data return data
@_register_writer(1) # Basic type, except for the legacy API. @_register_writer(1) # Basic type, except for the legacy API.
@ -767,7 +767,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return data return data
@_register_loader(2, 1) @_register_loader(2, 1)
def load_string(self, data, legacy_api=True): def load_string(self, data: bytes, legacy_api: bool = True) -> str:
if data.endswith(b"\0"): if data.endswith(b"\0"):
data = data[:-1] data = data[:-1]
return data.decode("latin-1", "replace") return data.decode("latin-1", "replace")
@ -797,7 +797,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
) )
@_register_loader(7, 1) @_register_loader(7, 1)
def load_undefined(self, data, legacy_api=True): def load_undefined(self, data, legacy_api: bool = True):
return data return data
@_register_writer(7) @_register_writer(7)
@ -809,7 +809,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return value return value
@_register_loader(10, 8) @_register_loader(10, 8)
def load_signed_rational(self, data, legacy_api=True): def load_signed_rational(self, data, legacy_api: bool = True):
vals = self._unpack(f"{len(data) // 4}l", data) vals = self._unpack(f"{len(data) // 4}l", data)
def combine(a, b): def combine(a, b):
@ -1030,7 +1030,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
"""Dictionary of tag types""" """Dictionary of tag types"""
@classmethod @classmethod
def from_v2(cls, original) -> ImageFileDirectory_v1: def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1:
"""Returns an """Returns an
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
instance with the same data as is contained in the original instance with the same data as is contained in the original
@ -1073,7 +1073,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __iter__(self): def __iter__(self):
return iter(set(self._tagdata) | set(self._tags_v1)) return iter(set(self._tagdata) | set(self._tags_v1))
def __setitem__(self, tag, value) -> None: def __setitem__(self, tag: int, value) -> None:
for legacy_api in (False, True): for legacy_api in (False, True):
self._setitem(tag, value, legacy_api) self._setitem(tag, value, legacy_api)
@ -1212,7 +1212,7 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number""" """Return the current frame number"""
return self.__frame return self.__frame
def get_photoshop_blocks(self): def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]:
""" """
Returns a dictionary of Photoshop "Image Resource Blocks". Returns a dictionary of Photoshop "Image Resource Blocks".
The keys are the image resource ID. For more information, see The keys are the image resource ID. For more information, see
@ -1259,7 +1259,7 @@ class TiffImageFile(ImageFile.ImageFile):
if ExifTags.Base.Orientation in self.tag_v2: if ExifTags.Base.Orientation in self.tag_v2:
del self.tag_v2[ExifTags.Base.Orientation] del self.tag_v2[ExifTags.Base.Orientation]
def _load_libtiff(self): def _load_libtiff(self) -> Image.core.PixelAccess | None:
"""Overload method triggered when we detect a compressed tiff """Overload method triggered when we detect a compressed tiff
Calls out to libtiff""" Calls out to libtiff"""

View File

@ -32,17 +32,24 @@ class _TagInfo(NamedTuple):
class TagInfo(_TagInfo): class TagInfo(_TagInfo):
__slots__: list[str] = [] __slots__: list[str] = []
def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): def __new__(
cls,
value: int | None = None,
name: str = "unknown",
type: int | None = None,
length: int | None = None,
enum: dict[str, int] | None = None,
) -> TagInfo:
return super().__new__(cls, value, name, type, length, enum or {}) return super().__new__(cls, value, name, type, length, enum or {})
def cvt_enum(self, value): def cvt_enum(self, value: str) -> int | str:
# Using get will call hash(value), which can be expensive # Using get will call hash(value), which can be expensive
# for some types (e.g. Fraction). Since self.enum is rarely # for some types (e.g. Fraction). Since self.enum is rarely
# used, it's usually better to test it first. # used, it's usually better to test it first.
return self.enum.get(value, value) if self.enum else value return self.enum.get(value, value) if self.enum else value
def lookup(tag, group=None): def lookup(tag: int, group: int | None = None) -> TagInfo:
""" """
:param tag: Integer tag number :param tag: Integer tag number
:param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in
@ -89,7 +96,7 @@ DOUBLE = 12
IFD = 13 IFD = 13
LONG8 = 16 LONG8 = 16
_tags_v2 = { _tags_v2: dict[int, tuple[str, int, int] | tuple[str, int, int, dict[str, int]]] = {
254: ("NewSubfileType", LONG, 1), 254: ("NewSubfileType", LONG, 1),
255: ("SubfileType", SHORT, 1), 255: ("SubfileType", SHORT, 1),
256: ("ImageWidth", LONG, 1), 256: ("ImageWidth", LONG, 1),
@ -233,7 +240,7 @@ _tags_v2 = {
50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one
50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006
} }
TAGS_V2_GROUPS = { _tags_v2_groups = {
# ExifIFD # ExifIFD
34665: { 34665: {
36864: ("ExifVersion", UNDEFINED, 1), 36864: ("ExifVersion", UNDEFINED, 1),
@ -281,7 +288,7 @@ TAGS_V2_GROUPS = {
# Legacy Tags structure # Legacy Tags structure
# these tags aren't included above, but were in the previous versions # these tags aren't included above, but were in the previous versions
TAGS = { TAGS: dict[int | tuple[int, int], str] = {
347: "JPEGTables", 347: "JPEGTables",
700: "XMP", 700: "XMP",
# Additional Exif Info # Additional Exif Info
@ -426,9 +433,10 @@ TAGS = {
} }
TAGS_V2: dict[int, TagInfo] = {} TAGS_V2: dict[int, TagInfo] = {}
TAGS_V2_GROUPS: dict[int, dict[int, TagInfo]] = {}
def _populate(): def _populate() -> None:
for k, v in _tags_v2.items(): for k, v in _tags_v2.items():
# Populate legacy structure. # Populate legacy structure.
TAGS[k] = v[0] TAGS[k] = v[0]
@ -438,9 +446,8 @@ def _populate():
TAGS_V2[k] = TagInfo(k, *v) TAGS_V2[k] = TagInfo(k, *v)
for tags in TAGS_V2_GROUPS.values(): for group, tags in _tags_v2_groups.items():
for k, v in tags.items(): TAGS_V2_GROUPS[group] = {k: TagInfo(k, *v) for k, v in tags.items()}
tags[k] = TagInfo(k, *v)
_populate() _populate()