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

View File

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

View File

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