Added type hints

This commit is contained in:
Andrew Murray 2024-02-17 10:45:52 +11:00
parent 912a33f5e9
commit 5c858d75e4
4 changed files with 139 additions and 74 deletions

View File

@ -1430,7 +1430,7 @@ class Image:
root = ElementTree.fromstring(xmp_tags) root = ElementTree.fromstring(xmp_tags)
return {get_name(root.tag): get_value(root)} return {get_name(root.tag): get_value(root)}
def getexif(self): def getexif(self) -> Exif:
""" """
Gets EXIF data from the image. Gets EXIF data from the image.
@ -1438,7 +1438,6 @@ class Image:
""" """
if self._exif is None: if self._exif is None:
self._exif = Exif() self._exif = Exif()
self._exif._loaded = False
elif self._exif._loaded: elif self._exif._loaded:
return self._exif return self._exif
self._exif._loaded = True self._exif._loaded = True
@ -1525,7 +1524,7 @@ class Image:
self.load() self.load()
return self.im.ptr return self.im.ptr
def getpalette(self, rawmode="RGB"): def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None:
""" """
Returns the image palette as a list. Returns the image palette as a list.
@ -1615,7 +1614,7 @@ class Image:
x, y = self.im.getprojection() x, y = self.im.getprojection()
return list(x), list(y) return list(x), list(y)
def histogram(self, mask=None, extrema=None): def histogram(self, mask=None, extrema=None) -> list[int]:
""" """
Returns a histogram for the image. The histogram is returned as a Returns a histogram for the image. The histogram is returned as a
list of pixel counts, one for each pixel value in the source list of pixel counts, one for each pixel value in the source
@ -1804,7 +1803,7 @@ 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=None): def point(self, lut, mode: str | None = None) -> Image:
""" """
Maps this image through a lookup table or function. Maps this image through a lookup table or function.
@ -1928,7 +1927,7 @@ class Image:
self.im.putdata(data, scale, offset) self.im.putdata(data, scale, offset)
def putpalette(self, data, rawmode="RGB"): def putpalette(self, data, rawmode="RGB") -> None:
""" """
Attaches a palette to this image. The image must be a "P", "PA", "L" Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image. or "LA" image.
@ -2108,7 +2107,7 @@ class Image:
min(self.size[1], math.ceil(box[3] + support_y)), min(self.size[1], math.ceil(box[3] + support_y)),
) )
def resize(self, size, resample=None, box=None, reducing_gap=None): def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image:
""" """
Returns a resized copy of this image. Returns a resized copy of this image.
@ -2200,10 +2199,11 @@ class Image:
if factor_x > 1 or factor_y > 1: if factor_x > 1 or factor_y > 1:
reduce_box = self._get_safe_box(size, resample, box) reduce_box = self._get_safe_box(size, resample, box)
factor = (factor_x, factor_y) factor = (factor_x, factor_y)
if callable(self.reduce): self = (
self = self.reduce(factor, box=reduce_box) self.reduce(factor, box=reduce_box)
else: if callable(self.reduce)
self = Image.reduce(self, factor, box=reduce_box) else Image.reduce(self, factor, box=reduce_box)
)
box = ( box = (
(box[0] - reduce_box[0]) / factor_x, (box[0] - reduce_box[0]) / factor_x,
(box[1] - reduce_box[1]) / factor_y, (box[1] - reduce_box[1]) / factor_y,
@ -2818,7 +2818,7 @@ class Image:
self.im.transform2(box, image.im, method, data, resample, fill) self.im.transform2(box, image.im, method, data, resample, fill)
def transpose(self, method): def transpose(self, method: Transpose) -> Image:
""" """
Transpose image (flip or rotate in 90 degree steps) Transpose image (flip or rotate in 90 degree steps)
@ -2870,7 +2870,9 @@ class ImagePointHandler:
(for use with :py:meth:`~PIL.Image.Image.point`) (for use with :py:meth:`~PIL.Image.Image.point`)
""" """
pass @abc.abstractmethod
def point(self, im: Image) -> Image:
pass
class ImageTransformHandler: class ImageTransformHandler:
@ -3690,6 +3692,7 @@ class Exif(_ExifBase):
endian = None endian = None
bigtiff = False bigtiff = False
_loaded = False
def __init__(self): def __init__(self):
self._data = {} self._data = {}
@ -3805,7 +3808,7 @@ class Exif(_ExifBase):
return merged_dict return merged_dict
def tobytes(self, offset=8): def tobytes(self, offset: int = 8) -> bytes:
from . import TiffImagePlugin from . import TiffImagePlugin
head = self._get_head() head = self._get_head()
@ -3960,7 +3963,7 @@ class Exif(_ExifBase):
del self._info[tag] del self._info[tag]
self._data[tag] = value self._data[tag] = value
def __delitem__(self, tag): def __delitem__(self, tag: int) -> 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]
else: else:

View File

@ -124,7 +124,7 @@ def getrgb(color):
@lru_cache @lru_cache
def getcolor(color, mode): def getcolor(color, mode: str) -> tuple[int, ...]:
""" """
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is

View File

@ -21,6 +21,7 @@ from __future__ import annotations
import functools import functools
import operator import operator
import re import re
from typing import Protocol, Sequence, cast
from . import ExifTags, Image, ImagePalette from . import ExifTags, Image, ImagePalette
@ -28,7 +29,7 @@ from . import ExifTags, Image, ImagePalette
# helpers # helpers
def _border(border): def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
if isinstance(border, tuple): if isinstance(border, tuple):
if len(border) == 2: if len(border) == 2:
left, top = right, bottom = border left, top = right, bottom = border
@ -39,7 +40,7 @@ def _border(border):
return left, top, right, bottom return left, top, right, bottom
def _color(color, mode): def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
if isinstance(color, str): if isinstance(color, str):
from . import ImageColor from . import ImageColor
@ -47,7 +48,7 @@ def _color(color, mode):
return color return color
def _lut(image, lut): def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
if image.mode == "P": if image.mode == "P":
# FIXME: apply to lookup table, not image data # FIXME: apply to lookup table, not image data
msg = "mode P support coming soon" msg = "mode P support coming soon"
@ -65,7 +66,13 @@ def _lut(image, lut):
# actions # actions
def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): def autocontrast(
image: Image.Image,
cutoff: float | tuple[float, float] = 0,
ignore: int | Sequence[int] | None = None,
mask: Image.Image | None = None,
preserve_tone: bool = False,
) -> Image.Image:
""" """
Maximize (normalize) image contrast. This function calculates a Maximize (normalize) image contrast. This function calculates a
histogram of the input image (or mask region), removes ``cutoff`` percent of the histogram of the input image (or mask region), removes ``cutoff`` percent of the
@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
h = histogram[layer : layer + 256] h = histogram[layer : layer + 256]
if ignore is not None: if ignore is not None:
# get rid of outliers # get rid of outliers
try: if isinstance(ignore, int):
h[ignore] = 0 h[ignore] = 0
except TypeError: else:
# assume sequence
for ix in ignore: for ix in ignore:
h[ix] = 0 h[ix] = 0
if cutoff: if cutoff:
@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
for ix in range(256): for ix in range(256):
n = n + h[ix] n = n + h[ix]
# remove cutoff% pixels from the low end # remove cutoff% pixels from the low end
cut = n * cutoff[0] // 100 cut = int(n * cutoff[0] // 100)
for lo in range(256): for lo in range(256):
if cut > h[lo]: if cut > h[lo]:
cut = cut - h[lo] cut = cut - h[lo]
@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
if cut <= 0: if cut <= 0:
break break
# remove cutoff% samples from the high end # remove cutoff% samples from the high end
cut = n * cutoff[1] // 100 cut = int(n * cutoff[1] // 100)
for hi in range(255, -1, -1): for hi in range(255, -1, -1):
if cut > h[hi]: if cut > h[hi]:
cut = cut - h[hi] cut = cut - h[hi]
@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
return _lut(image, lut) return _lut(image, lut)
def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): def colorize(
image: Image.Image,
black: str | tuple[int, ...],
white: str | tuple[int, ...],
mid: str | int | tuple[int, ...] | None = None,
blackpoint: int = 0,
whitepoint: int = 255,
midpoint: int = 127,
) -> Image.Image:
""" """
Colorize grayscale image. Colorize grayscale image.
This function calculates a color wedge which maps all black pixels in This function calculates a color wedge which maps all black pixels in
@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
# Define colors from arguments # Define colors from arguments
black = _color(black, "RGB") rgb_black = cast(Sequence[int], _color(black, "RGB"))
white = _color(white, "RGB") rgb_white = cast(Sequence[int], _color(white, "RGB"))
if mid is not None: rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
mid = _color(mid, "RGB")
# Empty lists for the mapping # Empty lists for the mapping
red = [] red = []
@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
# Create the low-end values # Create the low-end values
for i in range(0, blackpoint): for i in range(0, blackpoint):
red.append(black[0]) red.append(rgb_black[0])
green.append(black[1]) green.append(rgb_black[1])
blue.append(black[2]) blue.append(rgb_black[2])
# Create the mapping (2-color) # Create the mapping (2-color)
if mid is None: if rgb_mid is None:
range_map = range(0, whitepoint - blackpoint) range_map = range(0, whitepoint - blackpoint)
for i in range_map: for i in range_map:
red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) red.append(
green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) )
green.append(
rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
)
blue.append(
rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
)
# Create the mapping (3-color) # Create the mapping (3-color)
else: else:
@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
range_map2 = range(0, whitepoint - midpoint) range_map2 = range(0, whitepoint - midpoint)
for i in range_map1: for i in range_map1:
red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) red.append(
green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) )
green.append(
rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
)
blue.append(
rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
)
for i in range_map2: for i in range_map2:
red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) green.append(
blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
)
blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
# Create the high-end values # Create the high-end values
for i in range(0, 256 - whitepoint): for i in range(0, 256 - whitepoint):
red.append(white[0]) red.append(rgb_white[0])
green.append(white[1]) green.append(rgb_white[1])
blue.append(white[2]) blue.append(rgb_white[2])
# Return converted image # Return converted image
image = image.convert("RGB") image = image.convert("RGB")
return _lut(image, red + green + blue) return _lut(image, red + green + blue)
def contain(image, size, method=Image.Resampling.BICUBIC): def contain(
image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
) -> Image.Image:
""" """
Returns a resized version of the image, set to the maximum width and height Returns a resized version of the image, set to the maximum width and height
within the requested size, while maintaining the original aspect ratio. within the requested size, while maintaining the original aspect ratio.
@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
return image.resize(size, resample=method) return image.resize(size, resample=method)
def cover(image, size, method=Image.Resampling.BICUBIC): def cover(
image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
) -> Image.Image:
""" """
Returns a resized version of the image, so that the requested size is Returns a resized version of the image, so that the requested size is
covered, while maintaining the original aspect ratio. covered, while maintaining the original aspect ratio.
@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC):
return image.resize(size, resample=method) return image.resize(size, resample=method)
def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): def pad(
image: Image.Image,
size: tuple[int, int],
method: int = Image.Resampling.BICUBIC,
color: str | int | tuple[int, ...] | None = None,
centering: tuple[float, float] = (0.5, 0.5),
) -> Image.Image:
""" """
Returns a resized and padded version of the image, expanded to fill the Returns a resized and padded version of the image, expanded to fill the
requested aspect ratio and size. requested aspect ratio and size.
@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5
return out return out
def crop(image, border=0): def crop(image: Image.Image, border: int = 0) -> Image.Image:
""" """
Remove border from image. The same amount of pixels are removed Remove border from image. The same amount of pixels are removed
from all four sides. This function works on all image modes. from all four sides. This function works on all image modes.
@ -349,7 +386,9 @@ def crop(image, border=0):
return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
def scale(image, factor, resample=Image.Resampling.BICUBIC): def scale(
image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
) -> Image.Image:
""" """
Returns a rescaled image by a specific factor given in parameter. Returns a rescaled image by a specific factor given in parameter.
A factor greater than 1 expands the image, between 0 and 1 contracts the A factor greater than 1 expands the image, between 0 and 1 contracts the
@ -372,7 +411,19 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC):
return image.resize(size, resample) return image.resize(size, resample)
def deform(image, deformer, resample=Image.Resampling.BILINEAR): class _SupportsGetMesh(Protocol):
def getmesh(
self, image: Image.Image
) -> list[
tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
]: ...
def deform(
image: Image.Image,
deformer: _SupportsGetMesh,
resample: int = Image.Resampling.BILINEAR,
) -> Image.Image:
""" """
Deform the image. Deform the image.
@ -388,7 +439,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR):
) )
def equalize(image, mask=None): def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
""" """
Equalize the image histogram. This function applies a non-linear Equalize the image histogram. This function applies a non-linear
mapping to the input image, in order to create a uniform mapping to the input image, in order to create a uniform
@ -419,7 +470,11 @@ def equalize(image, mask=None):
return _lut(image, lut) return _lut(image, lut)
def expand(image, border=0, fill=0): def expand(
image: Image.Image,
border: int | tuple[int, ...] = 0,
fill: str | int | tuple[int, ...] = 0,
) -> Image.Image:
""" """
Add border to the image Add border to the image
@ -445,7 +500,13 @@ def expand(image, border=0, fill=0):
return out return out
def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): def fit(
image: Image.Image,
size: tuple[int, int],
method: int = Image.Resampling.BICUBIC,
bleed: float = 0.0,
centering: tuple[float, float] = (0.5, 0.5),
) -> Image.Image:
""" """
Returns a resized and cropped version of the image, cropped to the Returns a resized and cropped version of the image, cropped to the
requested aspect ratio and size. requested aspect ratio and size.
@ -479,13 +540,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
# kevin@cazabon.com # kevin@cazabon.com
# https://www.cazabon.com # https://www.cazabon.com
# ensure centering is mutable centering_x, centering_y = centering
centering = list(centering)
if not 0.0 <= centering[0] <= 1.0: if not 0.0 <= centering_x <= 1.0:
centering[0] = 0.5 centering_x = 0.5
if not 0.0 <= centering[1] <= 1.0: if not 0.0 <= centering_y <= 1.0:
centering[1] = 0.5 centering_y = 0.5
if not 0.0 <= bleed < 0.5: if not 0.0 <= bleed < 0.5:
bleed = 0.0 bleed = 0.0
@ -522,8 +582,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
crop_height = live_size[0] / output_ratio crop_height = live_size[0] / output_ratio
# make the crop # make the crop
crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
@ -531,7 +591,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
return image.resize(size, method, box=crop) return image.resize(size, method, box=crop)
def flip(image): def flip(image: Image.Image) -> Image.Image:
""" """
Flip the image vertically (top to bottom). Flip the image vertically (top to bottom).
@ -541,7 +601,7 @@ def flip(image):
return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
def grayscale(image): def grayscale(image: Image.Image) -> Image.Image:
""" """
Convert the image to grayscale. Convert the image to grayscale.
@ -551,7 +611,7 @@ def grayscale(image):
return image.convert("L") return image.convert("L")
def invert(image): def invert(image: Image.Image) -> Image.Image:
""" """
Invert (negate) the image. Invert (negate) the image.
@ -562,7 +622,7 @@ def invert(image):
return image.point(lut) if image.mode == "1" else _lut(image, lut) return image.point(lut) if image.mode == "1" else _lut(image, lut)
def mirror(image): def mirror(image: Image.Image) -> Image.Image:
""" """
Flip image horizontally (left to right). Flip image horizontally (left to right).
@ -572,7 +632,7 @@ def mirror(image):
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
def posterize(image, bits): def posterize(image: Image.Image, bits: int) -> Image.Image:
""" """
Reduce the number of bits for each color channel. Reduce the number of bits for each color channel.
@ -585,7 +645,7 @@ def posterize(image, bits):
return _lut(image, lut) return _lut(image, lut)
def solarize(image, threshold=128): def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
""" """
Invert all pixel values above a threshold. Invert all pixel values above a threshold.
@ -602,7 +662,7 @@ def solarize(image, threshold=128):
return _lut(image, lut) return _lut(image, lut)
def exif_transpose(image, *, in_place=False): def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
""" """
If an image has an EXIF Orientation tag, other than 1, transpose the image If an image has an EXIF Orientation tag, other than 1, transpose the image
accordingly, and remove the orientation data. accordingly, and remove the orientation data.
@ -616,7 +676,7 @@ def exif_transpose(image, *, in_place=False):
""" """
image.load() image.load()
image_exif = image.getexif() image_exif = image.getexif()
orientation = image_exif.get(ExifTags.Base.Orientation) orientation = image_exif.get(ExifTags.Base.Orientation, 1)
method = { method = {
2: Image.Transpose.FLIP_LEFT_RIGHT, 2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.Transpose.ROTATE_180, 3: Image.Transpose.ROTATE_180,
@ -653,3 +713,4 @@ def exif_transpose(image, *, in_place=False):
return transposed_image return transposed_image
elif not in_place: elif not in_place:
return image.copy() return image.copy()
return None

View File

@ -18,6 +18,7 @@
from __future__ import annotations from __future__ import annotations
import array import array
from typing import Sequence
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
@ -34,11 +35,11 @@ class ImagePalette:
Defaults to an empty palette. Defaults to an empty palette.
""" """
def __init__(self, mode="RGB", palette=None): def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None:
self.mode = mode self.mode = mode
self.rawmode = None # if set, palette contains raw data self.rawmode = None # if set, palette contains raw data
self.palette = palette or bytearray() self.palette = palette or bytearray()
self.dirty = None self.dirty: int | None = None
@property @property
def palette(self): def palette(self):
@ -127,7 +128,7 @@ class ImagePalette:
raise ValueError(msg) from e raise ValueError(msg) from e
return index return index
def getcolor(self, color, image=None): def getcolor(self, color, image=None) -> int:
"""Given an rgb tuple, allocate palette entry. """Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental. .. warning:: This method is experimental.