Merge pull request #8150 from radarhere/type_hint_image

Added type hints to Image
This commit is contained in:
Andrew Murray 2024-06-19 08:37:25 +10:00 committed by GitHub
commit b1d5d7f6f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 124 additions and 76 deletions

View File

@ -115,7 +115,11 @@ class FitsImageFile(ImageFile.ImageFile):
elif number_of_bits in (-32, -64):
self._mode = "F"
args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,)
args: tuple[str | int, ...]
if decoder_name == "raw":
args = (self.mode, 0, -1)
else:
args = (number_of_bits,)
return decoder_name, offset, args

View File

@ -458,6 +458,8 @@ class GifImageFile(ImageFile.ImageFile):
frame_im = self.im.convert("RGBA")
else:
frame_im = self.im.convert("RGB")
assert self.dispose_extent is not None
frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im

View File

@ -410,7 +410,9 @@ def init() -> bool:
# Codec factories (used by tobytes/frombytes and ImageFile.load)
def _getdecoder(mode, decoder_name, args, extra=()):
def _getdecoder(
mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = ()
) -> core.ImagingDecoder | ImageFile.PyDecoder:
# tweak arguments
if args is None:
args = ()
@ -433,7 +435,9 @@ def _getdecoder(mode, decoder_name, args, extra=()):
return decoder(mode, *args + extra)
def _getencoder(mode, encoder_name, args, extra=()):
def _getencoder(
mode: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = ()
) -> core.ImagingEncoder | ImageFile.PyEncoder:
# tweak arguments
if args is None:
args = ()
@ -550,10 +554,10 @@ class Image:
return self._size
@property
def mode(self):
def mode(self) -> str:
return self._mode
def _new(self, im) -> Image:
def _new(self, im: core.ImagingCore) -> Image:
new = Image()
new.im = im
new._mode = im.mode
@ -687,7 +691,7 @@ class Image:
)
)
def _repr_image(self, image_format, **kwargs):
def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None:
"""Helper function for iPython display hook.
:param image_format: Image format.
@ -700,14 +704,14 @@ class Image:
return None
return b.getvalue()
def _repr_png_(self):
def _repr_png_(self) -> bytes | None:
"""iPython display hook support for PNG format.
:returns: PNG version of the image as bytes
"""
return self._repr_image("PNG", compress_level=1)
def _repr_jpeg_(self):
def _repr_jpeg_(self) -> bytes | None:
"""iPython display hook support for JPEG format.
:returns: JPEG version of the image as bytes
@ -754,7 +758,7 @@ class Image:
self.putpalette(palette)
self.frombytes(data)
def tobytes(self, encoder_name: str = "raw", *args) -> bytes:
def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes:
"""
Return image as a bytes object.
@ -776,12 +780,13 @@ class Image:
:returns: A :py:class:`bytes` object.
"""
# may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple):
args = args[0]
encoder_args: Any = args
if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple):
# may pass tuple instead of argument list
encoder_args = encoder_args[0]
if encoder_name == "raw" and args == ():
args = self.mode
if encoder_name == "raw" and encoder_args == ():
encoder_args = self.mode
self.load()
@ -789,7 +794,7 @@ class Image:
return b""
# unpack data
e = _getencoder(self.mode, encoder_name, args)
e = _getencoder(self.mode, encoder_name, encoder_args)
e.setimage(self.im)
bufsize = max(65536, self.size[0] * 4) # see RawEncode.c
@ -832,7 +837,9 @@ class Image:
]
)
def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None:
def frombytes(
self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any
) -> None:
"""
Loads this image with pixel data from a bytes object.
@ -843,16 +850,17 @@ class Image:
if self.width == 0 or self.height == 0:
return
# may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple):
args = args[0]
decoder_args: Any = args
if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
# may pass tuple instead of argument list
decoder_args = decoder_args[0]
# default format
if decoder_name == "raw" and args == ():
args = self.mode
if decoder_name == "raw" and decoder_args == ():
decoder_args = self.mode
# unpack data
d = _getdecoder(self.mode, decoder_name, args)
d = _getdecoder(self.mode, decoder_name, decoder_args)
d.setimage(self.im)
s = d.decode(data)
@ -996,9 +1004,11 @@ class Image:
if has_transparency and self.im.bands == 3:
transparency = new_im.info["transparency"]
def convert_transparency(m, v):
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
return max(0, min(255, int(v)))
def convert_transparency(
m: tuple[float, ...], v: tuple[int, int, int]
) -> int:
value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
return max(0, min(255, int(value)))
if mode == "L":
transparency = convert_transparency(matrix, transparency)
@ -1250,7 +1260,7 @@ class Image:
__copy__ = copy
def crop(self, box: tuple[int, int, int, int] | None = None) -> Image:
def crop(self, box: tuple[float, float, float, float] | None = None) -> Image:
"""
Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel
@ -1276,7 +1286,9 @@ class Image:
self.load()
return self._new(self._crop(self.im, box))
def _crop(self, im, box):
def _crop(
self, im: core.ImagingCore, box: tuple[float, float, float, float]
) -> core.ImagingCore:
"""
Returns a rectangular region from the core image object im.
@ -1448,7 +1460,7 @@ class Image:
return self.im.getextrema()
def _getxmp(self, xmp_tags):
def get_name(tag):
def get_name(tag: str) -> str:
return re.sub("^{[^}]+}", "", tag)
def get_value(element):
@ -1549,7 +1561,11 @@ class Image:
fp = io.BytesIO(data)
with open(fp) as im:
if thumbnail_offset is None:
from . import TiffImagePlugin
if thumbnail_offset is None and isinstance(
im, TiffImagePlugin.TiffImageFile
):
im._frame_pos = [ifd_offset]
im._seek(0)
im.load()
@ -1803,7 +1819,9 @@ class Image:
else:
self.im.paste(im, box)
def alpha_composite(self, im, dest=(0, 0), source=(0, 0)):
def alpha_composite(
self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0)
) -> None:
"""'In-place' analog of Image.alpha_composite. Composites an image
onto this image.
@ -1818,32 +1836,35 @@ class Image:
"""
if not isinstance(source, (list, tuple)):
msg = "Source must be a tuple"
msg = "Source must be a list or tuple"
raise ValueError(msg)
if not isinstance(dest, (list, tuple)):
msg = "Destination must be a tuple"
msg = "Destination must be a list or tuple"
raise ValueError(msg)
if len(source) not in (2, 4):
msg = "Source must be a 2 or 4-tuple"
if len(source) == 4:
overlay_crop_box = tuple(source)
elif len(source) == 2:
overlay_crop_box = tuple(source) + im.size
else:
msg = "Source must be a sequence of length 2 or 4"
raise ValueError(msg)
if not len(dest) == 2:
msg = "Destination must be a 2-tuple"
msg = "Destination must be a sequence of length 2"
raise ValueError(msg)
if min(source) < 0:
msg = "Source must be non-negative"
raise ValueError(msg)
if len(source) == 2:
source = source + im.size
# over image, crop if it's not the whole thing.
if source == (0, 0) + im.size:
# over image, crop if it's not the whole image.
if overlay_crop_box == (0, 0) + im.size:
overlay = im
else:
overlay = im.crop(source)
overlay = im.crop(overlay_crop_box)
# target for the paste
box = dest + (dest[0] + overlay.width, dest[1] + overlay.height)
box = tuple(dest) + (dest[0] + overlay.width, dest[1] + overlay.height)
# destination image. don't copy if we're using the whole image.
if box == (0, 0) + self.size:
@ -1854,7 +1875,11 @@ class Image:
result = alpha_composite(background, overlay)
self.paste(result, box)
def point(self, lut, mode: str | None = None) -> Image:
def point(
self,
lut: Sequence[float] | Callable[[int], float] | ImagePointHandler,
mode: str | None = None,
) -> Image:
"""
Maps this image through a lookup table or function.
@ -1891,7 +1916,9 @@ class Image:
scale, offset = _getscaleoffset(lut)
return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table
lut = [lut(i) for i in range(256)] * self.im.bands
flatLut = [lut(i) for i in range(256)] * self.im.bands
else:
flatLut = lut
if self.mode == "F":
# FIXME: _imaging returns a confusing error message for this case
@ -1899,8 +1926,8 @@ class Image:
raise ValueError(msg)
if mode != "F":
lut = [round(i) for i in lut]
return self._new(self.im.point(lut, mode))
flatLut = [round(i) for i in flatLut]
return self._new(self.im.point(flatLut, mode))
def putalpha(self, alpha):
"""
@ -2973,29 +3000,29 @@ def _wedge() -> Image:
return Image()._new(core.wedge("L"))
def _check_size(size):
def _check_size(size: Any) -> None:
"""
Common check to enforce type and sanity check on size tuples
:param size: Should be a 2 tuple of (width, height)
:returns: True, or raises a ValueError
:returns: None, or raises a ValueError
"""
if not isinstance(size, (list, tuple)):
msg = "Size must be a tuple"
msg = "Size must be a list or tuple"
raise ValueError(msg)
if len(size) != 2:
msg = "Size must be a tuple of length 2"
msg = "Size must be a sequence of length 2"
raise ValueError(msg)
if size[0] < 0 or size[1] < 0:
msg = "Width and height must be >= 0"
raise ValueError(msg)
return True
def new(
mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0
mode: str,
size: tuple[int, int] | list[int],
color: float | tuple[float, ...] | str | None = 0,
) -> Image:
"""
Creates a new image with the given mode and size.
@ -3044,7 +3071,13 @@ def new(
return im._new(core.fill(mode, size, color))
def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image:
def frombytes(
mode: str,
size: tuple[int, int],
data: bytes | bytearray,
decoder_name: str = "raw",
*args: Any,
) -> Image:
"""
Creates a copy of an image memory from pixel data in a buffer.
@ -3072,18 +3105,21 @@ def frombytes(mode, size, data, decoder_name: str = "raw", *args) -> Image:
im = new(mode, size)
if im.width != 0 and im.height != 0:
# may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple):
args = args[0]
decoder_args: Any = args
if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple):
# may pass tuple instead of argument list
decoder_args = decoder_args[0]
if decoder_name == "raw" and args == ():
args = mode
if decoder_name == "raw" and decoder_args == ():
decoder_args = mode
im.frombytes(data, decoder_name, args)
im.frombytes(data, decoder_name, decoder_args)
return im
def frombuffer(mode: str, size, data, decoder_name: str = "raw", *args) -> Image:
def frombuffer(
mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any
) -> Image:
"""
Creates an image memory referencing pixel data in a byte buffer.
@ -3540,7 +3576,7 @@ def merge(mode: str, bands: Sequence[Image]) -> Image:
def register_open(
id,
id: str,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool | str] | None = None,
) -> None:
@ -3674,7 +3710,7 @@ def _show(image: Image, **options: Any) -> None:
def effect_mandelbrot(
size: tuple[int, int], extent: tuple[int, int, int, int], quality: int
size: tuple[int, int], extent: tuple[float, float, float, float], quality: int
) -> Image:
"""
Generate a Mandelbrot set covering the given extent.
@ -3721,19 +3757,18 @@ def radial_gradient(mode: str) -> Image:
# Resources
def _apply_env_variables(env=None) -> None:
if env is None:
env = os.environ
def _apply_env_variables(env: dict[str, str] | None = None) -> None:
env_dict = env if env is not None else os.environ
for var_name, setter in [
("PILLOW_ALIGNMENT", core.set_alignment),
("PILLOW_BLOCK_SIZE", core.set_block_size),
("PILLOW_BLOCKS_MAX", core.set_blocks_max),
]:
if var_name not in env:
if var_name not in env_dict:
continue
var = env[var_name].lower()
var = env_dict[var_name].lower()
units = 1
for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]:
@ -3742,13 +3777,13 @@ def _apply_env_variables(env=None) -> None:
var = var[: -len(postfix)]
try:
var = int(var) * units
var_int = int(var) * units
except ValueError:
warnings.warn(f"{var_name} is not int")
continue
try:
setter(var)
setter(var_int)
except ValueError as e:
warnings.warn(f"{var_name}: {e}")

View File

@ -122,7 +122,7 @@ def _parse_codestream(fp):
elif csiz == 4:
mode = "RGBA"
else:
mode = None
mode = ""
return size, mode
@ -237,7 +237,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
msg = "not a JPEG 2000 file"
raise SyntaxError(msg)
if self.size is None or self.mode is None:
if self.size is None or not self.mode:
msg = "unable to determine size/mode"
raise SyntaxError(msg)

View File

@ -129,15 +129,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# and invert it because
# Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"]
im = im.point(
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift)
)
maxval = (1 << bpp) - 1
shift = 8 - bpp
im = im.point(lambda x: maxval - (x >> shift))
elif im.info.get("bpp") in (1, 2, 4):
# here we assume that even though the inherent mode is 8-bit grayscale,
# only the lower bpp bits are significant.
# We invert them to match the Palm.
bpp = im.info["bpp"]
im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval))
maxval = (1 << bpp) - 1
im = im.point(lambda x: maxval - (x & maxval))
else:
msg = f"cannot write mode {im.mode} as Palm"
raise OSError(msg)

View File

@ -12,5 +12,11 @@ class ImagingDraw:
class PixelAccess:
def __getattr__(self, name: str) -> Any: ...
class ImagingDecoder:
def __getattr__(self, name: str) -> Any: ...
class ImagingEncoder:
def __getattr__(self, name: str) -> Any: ...
def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ...
def __getattr__(name: str) -> Any: ...