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): elif number_of_bits in (-32, -64):
self._mode = "F" 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 return decoder_name, offset, args

View File

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

View File

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

View File

@ -122,7 +122,7 @@ def _parse_codestream(fp):
elif csiz == 4: elif csiz == 4:
mode = "RGBA" mode = "RGBA"
else: else:
mode = None mode = ""
return size, mode return size, mode
@ -237,7 +237,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
msg = "not a JPEG 2000 file" msg = "not a JPEG 2000 file"
raise SyntaxError(msg) 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" msg = "unable to determine size/mode"
raise SyntaxError(msg) 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 # and invert it because
# Palm does grayscale from white (0) to black (1) # Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"] bpp = im.encoderinfo["bpp"]
im = im.point( maxval = (1 << bpp) - 1
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift) shift = 8 - bpp
) im = im.point(lambda x: maxval - (x >> shift))
elif im.info.get("bpp") in (1, 2, 4): elif im.info.get("bpp") in (1, 2, 4):
# here we assume that even though the inherent mode is 8-bit grayscale, # here we assume that even though the inherent mode is 8-bit grayscale,
# only the lower bpp bits are significant. # only the lower bpp bits are significant.
# We invert them to match the Palm. # We invert them to match the Palm.
bpp = im.info["bpp"] 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: else:
msg = f"cannot write mode {im.mode} as Palm" msg = f"cannot write mode {im.mode} as Palm"
raise OSError(msg) raise OSError(msg)

View File

@ -12,5 +12,11 @@ class ImagingDraw:
class PixelAccess: class PixelAccess:
def __getattr__(self, name: str) -> Any: ... 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 font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ...
def __getattr__(name: str) -> Any: ... def __getattr__(name: str) -> Any: ...