Merge pull request #8046 from srittau/type-annotations

Add various type annotations
This commit is contained in:
Andrew Murray 2024-06-08 18:38:21 +10:00 committed by GitHub
commit 5bacce9dc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 249 additions and 97 deletions

View File

@ -124,8 +124,8 @@ def test_fastpath_translate() -> None:
def test_center() -> None:
im = hopper()
rotate(im, im.mode, 45, center=(0, 0))
rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0))
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0))
rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0))
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0))
def test_rotate_no_fill() -> None:

View File

@ -144,10 +144,12 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. py:currentmodule:: PIL.Image
.. data:: Resampling.NEAREST
:noindex:
Pick one nearest pixel from the input image. Ignore all other input pixels.
.. data:: Resampling.BOX
:noindex:
Each pixel of source image contributes to one pixel of the
destination image with identical weights.
@ -158,6 +160,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. versionadded:: 3.4.0
.. data:: Resampling.BILINEAR
:noindex:
For resize calculate the output pixel value using linear interpolation
on all pixels that may contribute to the output value.
@ -165,6 +168,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
in the input image is used.
.. data:: Resampling.HAMMING
:noindex:
Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have
dislocations on local level like with :data:`Resampling.BOX`.
@ -174,6 +178,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. versionadded:: 3.4.0
.. data:: Resampling.BICUBIC
:noindex:
For resize calculate the output pixel value using cubic interpolation
on all pixels that may contribute to the output value.
@ -181,6 +186,7 @@ pixel, the Python Imaging Library provides different resampling *filters*.
in the input image is used.
.. data:: Resampling.LANCZOS
:noindex:
Calculate the output pixel value using a high-quality Lanczos filter (a
truncated sinc) on all pixels that may contribute to the output value.

View File

@ -78,8 +78,6 @@ Constructing images
^^^^^^^^^^^^^^^^^^^
.. autofunction:: new
.. autoclass:: SupportsArrayInterface
:show-inheritance:
.. autofunction:: fromarray
.. autofunction:: frombytes
.. autofunction:: frombuffer
@ -365,6 +363,14 @@ Classes
.. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImageTransformHandler
Protocols
---------
.. autoclass:: SupportsArrayInterface
:show-inheritance:
.. autoclass:: SupportsGetData
:show-inheritance:
Constants
---------
@ -418,7 +424,6 @@ See :ref:`concept-filters` for details.
.. autoclass:: Resampling
:members:
:undoc-members:
:noindex:
Dither modes
^^^^^^^^^^^^

View File

@ -503,6 +503,12 @@ def _getscaleoffset(expr):
# Implementation wrapper
class SupportsGetData(Protocol):
def getdata(
self,
) -> tuple[Transform, Sequence[int]]: ...
class Image:
"""
This class represents an image object. To create
@ -1289,7 +1295,7 @@ class Image:
return im.crop((x0, y0, x1, y1))
def draft(
self, mode: str, size: tuple[int, int]
self, mode: str | None, size: tuple[int, int]
) -> tuple[str, tuple[int, int, float, float]] | None:
"""
Configures the image file loader so it returns a version of the
@ -1709,7 +1715,12 @@ class Image:
return self.im.entropy(extrema)
return self.im.entropy()
def paste(self, im, box=None, mask=None) -> None:
def paste(
self,
im: Image | str | float | tuple[float, ...],
box: tuple[int, int, int, int] | tuple[int, int] | None = None,
mask: Image | None = None,
) -> None:
"""
Pastes another image into this image. The box argument is either
a 2-tuple giving the upper left corner, a 4-tuple defining the
@ -1737,7 +1748,7 @@ class Image:
See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to
combine images with respect to their alpha channels.
:param im: Source image or pixel value (integer or tuple).
:param im: Source image or pixel value (integer, float or tuple).
:param box: An optional 4-tuple giving the region to paste into.
If a 2-tuple is used instead, it's treated as the upper left
corner. If omitted or None, the source is pasted into the
@ -2146,7 +2157,13 @@ class Image:
min(self.size[1], math.ceil(box[3] + support_y)),
)
def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image:
def resize(
self,
size: tuple[int, int],
resample: int | None = None,
box: tuple[float, float, float, float] | None = None,
reducing_gap: float | None = None,
) -> Image:
"""
Returns a resized copy of this image.
@ -2211,13 +2228,9 @@ class Image:
msg = "reducing_gap must be 1.0 or greater"
raise ValueError(msg)
size = tuple(size)
self.load()
if box is None:
box = (0, 0) + self.size
else:
box = tuple(box)
if self.size == size and box == (0, 0) + self.size:
return self.copy()
@ -2252,7 +2265,11 @@ class Image:
return self._new(self.im.resize(size, resample, box))
def reduce(self, factor, box=None):
def reduce(
self,
factor: int | tuple[int, int],
box: tuple[int, int, int, int] | None = None,
) -> Image:
"""
Returns a copy of the image reduced ``factor`` times.
If the size of the image is not dividable by ``factor``,
@ -2270,8 +2287,6 @@ class Image:
if box is None:
box = (0, 0) + self.size
else:
box = tuple(box)
if factor == (1, 1) and box == (0, 0) + self.size:
return self.copy()
@ -2287,13 +2302,13 @@ class Image:
def rotate(
self,
angle,
resample=Resampling.NEAREST,
expand=0,
center=None,
translate=None,
fillcolor=None,
):
angle: float,
resample: Resampling = Resampling.NEAREST,
expand: int | bool = False,
center: tuple[int, int] | None = None,
translate: tuple[int, int] | None = None,
fillcolor: float | tuple[float, ...] | str | None = None,
) -> Image:
"""
Returns a rotated copy of this image. This method returns a
copy of this image, rotated the given number of degrees counter
@ -2600,7 +2615,12 @@ class Image:
"""
return 0
def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0):
def thumbnail(
self,
size: tuple[float, float],
resample: Resampling = Resampling.BICUBIC,
reducing_gap: float = 2.0,
) -> None:
"""
Make this image into a thumbnail. This method modifies the
image to contain a thumbnail version of itself, no larger than
@ -2661,20 +2681,24 @@ class Image:
box = None
if reducing_gap is not None:
size = preserve_aspect_ratio()
if size is None:
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
size = preserved_size
res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap))
res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
)
if res is not None:
box = res[1]
if box is None:
self.load()
# load() may have changed the size of the image
size = preserve_aspect_ratio()
if size is None:
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
size = preserved_size
if self.size != size:
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
@ -2690,12 +2714,12 @@ class Image:
# instead of bloating the method docs, add a separate chapter.
def transform(
self,
size,
method,
data=None,
resample=Resampling.NEAREST,
fill=1,
fillcolor=None,
size: tuple[int, int],
method: Transform | ImageTransformHandler | SupportsGetData,
data: Sequence[Any] | None = None,
resample: int = Resampling.NEAREST,
fill: int = 1,
fillcolor: float | tuple[float, ...] | str | None = None,
) -> Image:
"""
Transforms this image. This method creates a new image with the
@ -2929,7 +2953,7 @@ class ImageTransformHandler:
self,
size: tuple[int, int],
image: Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]],
**options: Any,
) -> Image:
pass

View File

@ -34,7 +34,7 @@ from __future__ import annotations
import math
import numbers
import struct
from typing import TYPE_CHECKING, Sequence, cast
from typing import TYPE_CHECKING, AnyStr, Sequence, cast
from . import Image, ImageColor
from ._typing import Coords
@ -95,7 +95,9 @@ class ImageDraw:
if TYPE_CHECKING:
from . import ImageFont
def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
def getfont(
self,
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
"""
Get the current default font.
@ -120,14 +122,15 @@ class ImageDraw:
self.font = ImageFont.load_default()
return self.font
def _getfont(self, font_size: float | None):
def _getfont(
self, font_size: float | None
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
if font_size is not None:
from . import ImageFont
font = ImageFont.load_default(font_size)
return ImageFont.load_default(font_size)
else:
font = self.getfont()
return font
return self.getfont()
def _getink(self, ink, fill=None) -> tuple[int | None, int | None]:
if ink is None and fill is None:
@ -460,15 +463,13 @@ class ImageDraw:
right[3] -= r + 1
self.draw.draw_rectangle(right, ink, 1)
def _multiline_check(self, text) -> bool:
def _multiline_check(self, text: AnyStr) -> bool:
split_character = "\n" if isinstance(text, str) else b"\n"
return split_character in text
def _multiline_split(self, text) -> list[str | bytes]:
split_character = "\n" if isinstance(text, str) else b"\n"
return text.split(split_character)
def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
return text.split("\n" if isinstance(text, str) else b"\n")
def _multiline_spacing(self, font, spacing, stroke_width):
return (
@ -479,10 +480,15 @@ class ImageDraw:
def text(
self,
xy,
text,
xy: tuple[float, float],
text: str,
fill=None,
font=None,
font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
anchor=None,
spacing=4,
align="left",
@ -536,7 +542,7 @@ class ImageDraw:
coord.append(int(xy[i]))
start.append(math.modf(xy[i])[0])
try:
mask, offset = font.getmask2(
mask, offset = font.getmask2( # type: ignore[union-attr,misc]
text,
mode,
direction=direction,
@ -552,7 +558,7 @@ class ImageDraw:
coord = [coord[0] + offset[0], coord[1] + offset[1]]
except AttributeError:
try:
mask = font.getmask(
mask = font.getmask( # type: ignore[misc]
text,
mode,
direction,
@ -601,10 +607,15 @@ class ImageDraw:
def multiline_text(
self,
xy,
text,
xy: tuple[float, float],
text: str,
fill=None,
font=None,
font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
anchor=None,
spacing=4,
align="left",
@ -634,7 +645,7 @@ class ImageDraw:
font = self._getfont(font_size)
widths = []
max_width = 0
max_width: float = 0
lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines:
@ -688,15 +699,20 @@ class ImageDraw:
def textlength(
self,
text,
font=None,
text: str,
font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
) = None,
direction=None,
features=None,
language=None,
embedded_color=False,
*,
font_size=None,
):
) -> float:
"""Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text):
msg = "can't measure length of multiline text"
@ -788,7 +804,7 @@ class ImageDraw:
font = self._getfont(font_size)
widths = []
max_width = 0
max_width: float = 0
lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines:

View File

@ -33,11 +33,16 @@ import sys
import warnings
from enum import IntEnum
from io import BytesIO
from typing import BinaryIO
from typing import IO, TYPE_CHECKING, Any, BinaryIO
from . import Image
from ._typing import StrOrBytesPath
from ._util import is_directory, is_path
from ._util import is_path
if TYPE_CHECKING:
from . import ImageFile
from ._imaging import ImagingFont
from ._imagingft import Font
class Layout(IntEnum):
@ -56,7 +61,7 @@ except ImportError as ex:
core = DeferredError.new(ex)
def _string_length_check(text):
def _string_length_check(text: str | bytes | bytearray) -> None:
if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
msg = "too many characters in string"
raise ValueError(msg)
@ -81,9 +86,11 @@ def _string_length_check(text):
class ImageFont:
"""PIL font wrapper"""
def _load_pilfont(self, filename):
font: ImagingFont
def _load_pilfont(self, filename: str) -> None:
with open(filename, "rb") as fp:
image = None
image: ImageFile.ImageFile | None = None
for ext in (".png", ".gif", ".pbm"):
if image:
image.close()
@ -106,7 +113,7 @@ class ImageFont:
self._load_pilfont_data(fp, image)
image.close()
def _load_pilfont_data(self, file, image):
def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
# read PILfont header
if file.readline() != b"PILfont\n":
msg = "Not a PILfont file"
@ -153,7 +160,9 @@ class ImageFont:
Image._decompression_bomb_check(self.font.getsize(text))
return self.font.getmask(text, mode)
def getbbox(self, text, *args, **kwargs):
def getbbox(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
) -> tuple[int, int, int, int]:
"""
Returns bounding box (in pixels) of given text.
@ -167,7 +176,9 @@ class ImageFont:
width, height = self.font.getsize(text)
return 0, 0, width, height
def getlength(self, text, *args, **kwargs):
def getlength(
self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
) -> int:
"""
Returns length (in pixels) of given text.
This is the amount by which following text should be offset.
@ -187,6 +198,8 @@ class ImageFont:
class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)"""
font: Font
def __init__(
self,
font: StrOrBytesPath | BinaryIO | None = None,
@ -250,7 +263,7 @@ class FreeTypeFont:
path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine)
def getname(self):
def getname(self) -> tuple[str | None, str | None]:
"""
:return: A tuple of the font family (e.g. Helvetica) and the font style
(e.g. Bold)
@ -265,7 +278,9 @@ class FreeTypeFont:
"""
return self.font.ascent, self.font.descent
def getlength(self, text, mode="", direction=None, features=None, language=None):
def getlength(
self, text: str, mode="", direction=None, features=None, language=None
) -> float:
"""
Returns length (in pixels with 1/64 precision) of given text when rendered
in font with provided direction, features, and language.
@ -339,14 +354,14 @@ class FreeTypeFont:
def getbbox(
self,
text,
mode="",
direction=None,
features=None,
language=None,
stroke_width=0,
anchor=None,
):
text: str,
mode: str = "",
direction: str | None = None,
features: list[str] | None = None,
language: str | None = None,
stroke_width: float = 0,
anchor: str | None = None,
) -> tuple[float, float, float, float]:
"""
Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language.
@ -496,7 +511,7 @@ class FreeTypeFont:
def getmask2(
self,
text,
text: str,
mode="",
direction=None,
features=None,
@ -666,10 +681,11 @@ class FreeTypeFont:
msg = "FreeType 2.9.1 or greater is required"
raise NotImplementedError(msg) from e
for axis in axes:
axis["name"] = axis["name"].replace(b"\x00", b"")
if axis["name"]:
axis["name"] = axis["name"].replace(b"\x00", b"")
return axes
def set_variation_by_axes(self, axes):
def set_variation_by_axes(self, axes: list[float]) -> None:
"""
:param axes: A list of values for each axis.
:exception OSError: If the font is not a variation font.
@ -714,14 +730,14 @@ class TransposedFont:
return 0, 0, height, width
return 0, 0, width, height
def getlength(self, text, *args, **kwargs):
def getlength(self, text: str, *args, **kwargs) -> float:
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
msg = "text length is undefined for text rotated by 90 or 270 degrees"
raise ValueError(msg)
return self.font.getlength(text, *args, **kwargs)
def load(filename):
def load(filename: str) -> ImageFont:
"""
Load a font file. This function loads a font object from the given
bitmap font file, and returns the corresponding font object.
@ -735,7 +751,13 @@ def load(filename):
return f
def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
def truetype(
font: StrOrBytesPath | BinaryIO | None = None,
size: float = 10,
index: int = 0,
encoding: str = "",
layout_engine: Layout | None = None,
) -> FreeTypeFont:
"""
Load a TrueType or OpenType font from a file or file-like object,
and create a font object.
@ -796,7 +818,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
:exception ValueError: If the font size is not greater than zero.
"""
def freetype(font):
def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont:
return FreeTypeFont(font, size, index, encoding, layout_engine)
try:
@ -846,7 +868,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
raise
def load_path(filename):
def load_path(filename: str | bytes) -> ImageFont:
"""
Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a
bitmap font along the Python path.
@ -855,14 +877,13 @@ def load_path(filename):
:return: A font object.
:exception OSError: If the file could not be read.
"""
if not isinstance(filename, str):
filename = filename.decode("utf-8")
for directory in sys.path:
if is_directory(directory):
if not isinstance(filename, str):
filename = filename.decode("utf-8")
try:
return load(os.path.join(directory, filename))
except OSError:
pass
try:
return load(os.path.join(directory, filename))
except OSError:
pass
msg = "cannot find font file"
raise OSError(msg)
@ -881,6 +902,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
:return: A font object.
"""
f: FreeTypeFont | ImageFont
if core.__class__.__name__ == "module" or size is not None:
f = truetype(
BytesIO(

View File

@ -14,7 +14,7 @@
#
from __future__ import annotations
from typing import Sequence
from typing import Any, Sequence
from . import Image
@ -34,7 +34,7 @@ class Transform(Image.ImageTransformHandler):
self,
size: tuple[int, int],
image: Image.Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]],
**options: Any,
) -> Image.Image:
"""Perform the transform. Called from :py:meth:`.Image.transform`."""
# can be overridden

View File

@ -426,7 +426,7 @@ class JpegImageFile(ImageFile.ImageFile):
return s
def draft(
self, mode: str, size: tuple[int, int]
self, mode: str | None, size: tuple[int, int]
) -> tuple[str, tuple[int, int, float, float]] | None:
if len(self.tile) != 1:
return None

View File

@ -1,3 +1,16 @@
from typing import Any
class ImagingCore:
def __getattr__(self, name: str) -> Any: ...
class ImagingFont:
def __getattr__(self, name: str) -> Any: ...
class ImagingDraw:
def __getattr__(self, name: str) -> Any: ...
class PixelAccess:
def __getattr__(self, name: str) -> Any: ...
def font(image, glyphdata: bytes) -> ImagingFont: ...
def __getattr__(name: str) -> Any: ...

View File

@ -1,3 +1,69 @@
from typing import Any
from typing import Any, TypedDict
from . import _imaging
class _Axis(TypedDict):
minimum: int | None
default: int | None
maximum: int | None
name: bytes | None
class Font:
@property
def family(self) -> str | None: ...
@property
def style(self) -> str | None: ...
@property
def ascent(self) -> int: ...
@property
def descent(self) -> int: ...
@property
def height(self) -> int: ...
@property
def x_ppem(self) -> int: ...
@property
def y_ppem(self) -> int: ...
@property
def glyphs(self) -> int: ...
def render(
self,
string: str,
fill,
mode=...,
dir=...,
features=...,
lang=...,
stroke_width=...,
anchor=...,
foreground_ink_long=...,
x_start=...,
y_start=...,
/,
) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ...
def getsize(
self,
string: str | bytes | bytearray,
mode=...,
dir=...,
features=...,
lang=...,
anchor=...,
/,
) -> tuple[tuple[int, int], tuple[int, int]]: ...
def getlength(
self, string: str, mode=..., dir=..., features=..., lang=..., /
) -> float: ...
def getvarnames(self) -> list[bytes]: ...
def getvaraxes(self) -> list[_Axis] | None: ...
def setvarname(self, instance_index: int, /) -> None: ...
def setvaraxes(self, axes: list[float], /) -> None: ...
def getfont(
filename: str | bytes,
size: float,
index=...,
encoding=...,
font_bytes=...,
layout_engine=...,
) -> Font: ...
def __getattr__(name: str) -> Any: ...