Added _typing.Coords

This commit is contained in:
Andrew Murray 2024-02-06 07:49:43 +11:00
parent 5a8e7dda79
commit 65cb0b0487
3 changed files with 91 additions and 87 deletions

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import contextlib
import os.path
from typing import Sequence
import pytest
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
from PIL._typing import Coords
from .helper import (
assert_image_equal,
@ -75,7 +75,7 @@ def test_mode_mismatch() -> None:
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> None:
def test_arc(bbox: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -88,7 +88,7 @@ def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> N
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None:
def test_arc_end_le_start(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -103,7 +103,7 @@ def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None:
def test_arc_no_loops(bbox: Coords) -> None:
# No need to go in loops
# Arrange
im = Image.new("RGB", (W, H))
@ -119,7 +119,7 @@ def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_arc_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -132,7 +132,7 @@ def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None:
def test_arc_width_pieslice_large(bbox: Coords) -> None:
# Tests an arc with a large enough width that it is a pieslice
# Arrange
im = Image.new("RGB", (W, H))
@ -146,7 +146,7 @@ def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
def test_arc_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -159,7 +159,7 @@ def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_non_whole_angle(bbox: Sequence[int | Sequence[int]]) -> None:
def test_arc_width_non_whole_angle(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -201,7 +201,7 @@ def test_bitmap() -> None:
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX)
def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None:
def test_chord(mode: str, bbox: Coords) -> None:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@ -215,7 +215,7 @@ def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_chord_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -228,7 +228,7 @@ def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
def test_chord_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -241,7 +241,7 @@ def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_chord_zero_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_chord_zero_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -267,7 +267,7 @@ def test_chord_too_fat() -> None:
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None:
def test_ellipse(mode: str, bbox: Coords) -> None:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@ -281,7 +281,7 @@ def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_translucent(bbox: Sequence[int | Sequence[int]]) -> None:
def test_ellipse_translucent(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@ -318,7 +318,7 @@ def test_ellipse_symmetric() -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_ellipse_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -343,7 +343,7 @@ def test_ellipse_width_large() -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
def test_ellipse_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -356,7 +356,7 @@ def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_zero_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_ellipse_zero_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -410,12 +410,7 @@ def test_ellipse_various_sizes_filled() -> None:
@pytest.mark.parametrize("points", POINTS)
def test_line(
points: tuple[tuple[int, int], ...]
| list[tuple[int, int]]
| tuple[int, ...]
| list[int]
) -> None:
def test_line(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -488,9 +483,7 @@ def test_transform() -> None:
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(
bbox: Sequence[int | Sequence[int]], start: float, end: float
) -> None:
def test_pieslice(bbox: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -503,7 +496,7 @@ def test_pieslice(
@pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_pieslice_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -516,7 +509,7 @@ def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
def test_pieslice_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -530,7 +523,7 @@ def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_zero_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_pieslice_zero_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -585,12 +578,7 @@ def test_pieslice_no_spikes() -> None:
@pytest.mark.parametrize("points", POINTS)
def test_point(
points: tuple[tuple[int, int], ...]
| list[tuple[int, int]]
| tuple[int, ...]
| list[int]
) -> None:
def test_point(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -615,12 +603,7 @@ def test_point_I16() -> None:
@pytest.mark.parametrize("points", POINTS)
def test_polygon(
points: tuple[tuple[int, int], ...]
| list[tuple[int, int]]
| tuple[int, ...]
| list[int]
) -> None:
def test_polygon(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -693,7 +676,7 @@ def test_polygon_translucent() -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle(bbox: Sequence[int | Sequence[int]]) -> None:
def test_rectangle(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -720,7 +703,7 @@ def test_big_rectangle() -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_rectangle_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -734,7 +717,7 @@ def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
def test_rectangle_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -748,7 +731,7 @@ def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None:
def test_rectangle_zero_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -761,7 +744,7 @@ def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None:
def test_rectangle_I16(bbox: Coords) -> None:
# Arrange
im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im)
@ -774,7 +757,7 @@ def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_translucent_outline(bbox: Sequence[int | Sequence[int]]) -> None:
def test_rectangle_translucent_outline(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@ -866,7 +849,7 @@ def test_rounded_rectangle_non_integer_radius(
@pytest.mark.parametrize("bbox", BBOX)
def test_rounded_rectangle_zero_radius(bbox: Sequence[int | Sequence[int]]) -> None:
def test_rounded_rectangle_zero_radius(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -907,7 +890,7 @@ def test_rounded_rectangle_translucent(
@pytest.mark.parametrize("bbox", BBOX)
def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None:
def test_floodfill(bbox: Coords) -> None:
red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
@ -940,7 +923,7 @@ def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None:
def test_floodfill_border(bbox: Coords) -> None:
# floodfill() is experimental
# Arrange
@ -962,7 +945,7 @@ def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_floodfill_thresh(bbox: Sequence[int | Sequence[int]]) -> None:
def test_floodfill_thresh(bbox: Coords) -> None:
# floodfill() is experimental
# Arrange
@ -1419,7 +1402,7 @@ def test_default_font_size() -> None:
@pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox: Sequence[int | Sequence[int]]) -> None:
def test_same_color_outline(bbox: Coords) -> None:
# Prepare shape
x0, y0 = 5, 5
x1, y1 = 5, 50

View File

@ -34,8 +34,10 @@ from __future__ import annotations
import math
import numbers
import struct
from typing import Sequence, cast
from . import Image, ImageColor
from ._typing import Coords
"""
A simple 2D drawing interface for PIL images.
@ -145,13 +147,13 @@ class ImageDraw:
fill = self.draw.draw_ink(fill)
return ink, fill
def arc(self, xy, start, end, fill=None, width=1) -> None:
def arc(self, xy: Coords, start, end, fill=None, width=1) -> None:
"""Draw an arc."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_arc(xy, start, end, ink, width)
def bitmap(self, xy, bitmap, fill=None) -> None:
def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None:
"""Draw a bitmap."""
bitmap.load()
ink, fill = self._getink(fill)
@ -160,7 +162,7 @@ class ImageDraw:
if ink is not None:
self.draw.draw_bitmap(xy, bitmap.im, ink)
def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None:
def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None:
"""Draw a chord."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@ -168,7 +170,7 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0:
self.draw.draw_chord(xy, start, end, ink, 0, width)
def ellipse(self, xy, fill=None, outline=None, width=1) -> None:
def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw an ellipse."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@ -176,20 +178,29 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0:
self.draw.draw_ellipse(xy, ink, 0, width)
def line(self, xy, fill=None, width=0, joint=None) -> None:
def line(self, xy: Coords, fill=None, width=0, joint=None) -> None:
"""Draw a line, or a connected sequence of line segments."""
ink = self._getink(fill)[0]
if ink is not None:
self.draw.draw_lines(xy, ink, width)
if joint == "curve" and width > 4:
if not isinstance(xy[0], (list, tuple)):
xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)]
for i in range(1, len(xy) - 1):
point = xy[i]
points: Sequence[Sequence[float]]
if isinstance(xy[0], (list, tuple)):
points = cast(Sequence[Sequence[float]], xy)
else:
points = [
cast(Sequence[float], tuple(xy[i : i + 2]))
for i in range(0, len(xy), 2)
]
for i in range(1, len(points) - 1):
point = points[i]
angles = [
math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
% 360
for start, end in ((xy[i - 1], point), (point, xy[i + 1]))
for start, end in (
(points[i - 1], point),
(point, points[i + 1]),
)
]
if angles[0] == angles[1]:
# This is a straight line, so no joint is required
@ -245,7 +256,9 @@ class ImageDraw:
if ink is not None and ink != fill:
self.draw.draw_outline(shape, ink, 0)
def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None:
def pieslice(
self, xy: Coords, start, end, fill=None, outline=None, width=1
) -> None:
"""Draw a pieslice."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@ -253,13 +266,13 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0:
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
def point(self, xy, fill=None) -> None:
def point(self, xy: Coords, fill=None) -> None:
"""Draw one or more individual pixels."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_points(xy, ink)
def polygon(self, xy, fill=None, outline=None, width=1) -> None:
def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw a polygon."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@ -296,7 +309,7 @@ class ImageDraw:
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
self.polygon(xy, fill, outline, width)
def rectangle(self, xy, fill=None, outline=None, width=1) -> None:
def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw a rectangle."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@ -305,13 +318,13 @@ class ImageDraw:
self.draw.draw_rectangle(xy, ink, 0, width)
def rounded_rectangle(
self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None
self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None
) -> None:
"""Draw a rounded rectangle."""
if isinstance(xy[0], (list, tuple)):
(x0, y0), (x1, y1) = xy
(x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy)
else:
x0, y0, x1, y1 = xy
x0, y0, x1, y1 = cast(Sequence[float], xy)
if x1 < x0:
msg = "x1 must be greater than or equal to x0"
raise ValueError(msg)
@ -347,6 +360,7 @@ class ImageDraw:
ink, fill = self._getink(outline, fill)
def draw_corners(pieslice) -> None:
parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
if full_x:
# Draw top and bottom halves
parts = (
@ -361,17 +375,18 @@ class ImageDraw:
)
else:
# Draw four separate corners
parts = []
for i, part in enumerate(
(
((x0, y0, x0 + d, y0 + d), 180, 270),
((x1 - d, y0, x1, y0 + d), 270, 360),
((x1 - d, y1 - d, x1, y1), 0, 90),
((x0, y1 - d, x0 + d, y1), 90, 180),
parts = tuple(
part
for i, part in enumerate(
(
((x0, y0, x0 + d, y0 + d), 180, 270),
((x1 - d, y0, x1, y0 + d), 270, 360),
((x1 - d, y1 - d, x1, y1), 0, 90),
((x0, y1 - d, x0 + d, y1), 90, 180),
)
)
):
if corners[i]:
parts.append(part)
if corners[i]
)
for part in parts:
if pieslice:
self.draw.draw_pieslice(*(part + (fill, 1)))
@ -520,7 +535,7 @@ class ImageDraw:
*args,
**kwargs,
)
coord = coord[0] + offset[0], coord[1] + offset[1]
coord = [coord[0] + offset[0], coord[1] + offset[1]]
except AttributeError:
try:
mask = font.getmask(
@ -539,7 +554,7 @@ class ImageDraw:
except TypeError:
mask = font.getmask(text)
if stroke_offset:
coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]]
if mode == "RGBA":
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
# extract mask and set text alpha
@ -548,7 +563,9 @@ class ImageDraw:
color.fillband(3, ink_alpha)
x, y = coord
if self.im is not None:
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
self.im.paste(
color, (x, y, x + mask.size[0], y + mask.size[1]), mask
)
else:
self.draw.draw_bitmap(coord, mask, ink)
@ -829,7 +846,7 @@ class ImageDraw:
return bbox
def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
def Draw(im, mode: str | None = None) -> ImageDraw:
"""
A simple 2D drawing interface for PIL images.
@ -933,7 +950,9 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
edge = new_edge
def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> list[tuple[float, float]]:
def _compute_regular_polygon_vertices(
bounding_circle, n_sides, rotation
) -> list[tuple[float, float]]:
"""
Generate a list of vertices for a 2D regular polygon.
@ -1051,9 +1070,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> lis
angles = _get_angles(n_sides, rotation)
# 4. Compute Vertices
return [
_compute_polygon_vertex(angle) for angle in angles
]
return [_compute_polygon_vertex(angle) for angle in angles]
def _color_diff(color1, color2: float | tuple[int, ...]) -> float:

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import sys
from typing import Sequence, Union
if sys.version_info >= (3, 10):
from typing import TypeGuard
@ -15,4 +16,7 @@ else:
return bool
Coords = Union[Sequence[float], Sequence[Sequence[float]]]
__all__ = ["TypeGuard"]