mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 17:36:18 +03:00
Update regular_polygon
args + arg checks
Summary of changes - Allow positional args in `regular_polygon` method - Allow multiple bounding box formats - (e.g. bbox = [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]) - Check if bounding box is square - Update var names - b_box => bbox - nb_sides => n_sides
This commit is contained in:
parent
0ed01dd964
commit
ac7d41545d
|
@ -1095,10 +1095,10 @@ def test_same_color_outline():
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"nb_polygon_sides, rotation, polygon_name",
|
||||
"n_polygon_sides, rotation, polygon_name",
|
||||
[(4, 0, "square"), (8, 0, "octagon"), (4, 45, "square")],
|
||||
)
|
||||
def test_draw_regular_polygon(nb_polygon_sides, rotation, polygon_name):
|
||||
def test_draw_regular_polygon(n_polygon_sides, rotation, polygon_name):
|
||||
im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0))
|
||||
filename = (
|
||||
f"Tests/images/imagedraw_regular_polygon__{polygon_name}"
|
||||
|
@ -1106,13 +1106,13 @@ def test_draw_regular_polygon(nb_polygon_sides, rotation, polygon_name):
|
|||
)
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.regular_polygon(
|
||||
b_box=[(0, 0), (W, H)], nb_sides=nb_polygon_sides, rotation=rotation, fill="red"
|
||||
[(0, 0), (W, H)], n_polygon_sides, rotation=rotation, fill="red"
|
||||
)
|
||||
assert_image_equal(im, Image.open(filename))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"nb_polygon_sides, expected_vertices",
|
||||
"n_polygon_sides, expected_vertices",
|
||||
[
|
||||
(3, [(6.7, 75.0), (93.3, 75.0), (50.0, 0.0)]),
|
||||
(4, [(14.64, 85.36), (85.36, 85.36), (85.36, 14.64), (14.64, 14.64)]),
|
||||
|
@ -1139,40 +1139,49 @@ def test_draw_regular_polygon(nb_polygon_sides, rotation, polygon_name):
|
|||
),
|
||||
],
|
||||
)
|
||||
def test_compute_regular_polygon_vertices(nb_polygon_sides, expected_vertices):
|
||||
def test_compute_regular_polygon_vertices(n_polygon_sides, expected_vertices):
|
||||
vertices = ImageDraw._compute_regular_polygon_vertices(
|
||||
nb_sides=nb_polygon_sides, b_box=[(0, 0), (100, 100)], rotation=0
|
||||
n_sides=n_polygon_sides, bbox=[(0, 0), (100, 100)], rotation=0
|
||||
)
|
||||
assert vertices == expected_vertices
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"nb_polygon_sides, bounding_box, rotation, expected_error, error_message",
|
||||
"n_polygon_sides, bounding_box, rotation, expected_error, error_message",
|
||||
[
|
||||
(None, [(0, 0), (100, 100)], 0, TypeError, "nb_sides should be an int"),
|
||||
(1, [(0, 0), (100, 100)], 0, ValueError, "nb_sides should be an int > 2"),
|
||||
(3, 100, 0, TypeError, "b_box should be a list/tuple"),
|
||||
(None, [(0, 0), (100, 100)], 0, TypeError, "n_sides should be an int"),
|
||||
(1, [(0, 0), (100, 100)], 0, ValueError, "n_sides should be an int > 2"),
|
||||
(3, 100, 0, TypeError, "bbox should be a list/tuple"),
|
||||
(
|
||||
3,
|
||||
[(0, 0), (50, 50), (100, 100)],
|
||||
[(0, 0), (100,), (100,)],
|
||||
0,
|
||||
ValueError,
|
||||
"b_box should have 2 items (top-left & bottom-right coordinates)",
|
||||
"bbox should have the following format "
|
||||
"[(x0, y0), (x1, y1)] or [x0, y0, x1, y1]",
|
||||
),
|
||||
(
|
||||
3,
|
||||
[(50, 50), (100,)],
|
||||
0,
|
||||
ValueError,
|
||||
"bbox should contain top-left and bottom-right coordinates (2D)",
|
||||
),
|
||||
(
|
||||
3,
|
||||
[(50, 50), (0, None)],
|
||||
0,
|
||||
ValueError,
|
||||
"b_box should only contain numeric data",
|
||||
"bbox should only contain numeric data",
|
||||
),
|
||||
(
|
||||
3,
|
||||
[(50, 50), (0, 0)],
|
||||
0,
|
||||
ValueError,
|
||||
"b_box: Bottom-right coordinate should be larger than top-left coordinate",
|
||||
"bbox: Bottom-right coordinate should be larger than top-left coordinate",
|
||||
),
|
||||
(3, [(0, 0), (100, 90)], 0, ValueError, "bbox should be a square",),
|
||||
(
|
||||
3,
|
||||
[(0, 0), (100, 100)],
|
||||
|
@ -1183,10 +1192,10 @@ def test_compute_regular_polygon_vertices(nb_polygon_sides, expected_vertices):
|
|||
],
|
||||
)
|
||||
def test_compute_regular_polygon_vertices_input_error_handling(
|
||||
nb_polygon_sides, bounding_box, rotation, expected_error, error_message
|
||||
n_polygon_sides, bounding_box, rotation, expected_error, error_message
|
||||
):
|
||||
with pytest.raises(expected_error) as e:
|
||||
ImageDraw._compute_regular_polygon_vertices(
|
||||
nb_sides=nb_polygon_sides, b_box=bounding_box, rotation=rotation
|
||||
n_sides=n_polygon_sides, bbox=bounding_box, rotation=rotation
|
||||
)
|
||||
assert str(e.value) == error_message
|
||||
|
|
|
@ -255,15 +255,15 @@ Methods
|
|||
:param fill: Color to use for the fill.
|
||||
|
||||
|
||||
.. py:method:: ImageDraw.regular_polygon(*, b_box, nb_sides, rotation=0, fill=None, outline=None)
|
||||
.. py:method:: ImageDraw.regular_polygon(bbox, n_sides, rotation=0, fill=None, outline=None)
|
||||
|
||||
Draws a regular polygon inscribed in ``b_box``,
|
||||
with ``nb_sides``, and rotation of ``rotation`` degrees
|
||||
Draws a regular polygon inscribed in ``bbox``,
|
||||
with ``n_sides``, and rotation of ``rotation`` degrees
|
||||
|
||||
:param b_box: A bounding box which inscribes the polygon
|
||||
(e.g. b_box = [(50, 50), (150, 150)])
|
||||
:param nb_sides: Number of sides
|
||||
(e.g. nb_sides=3 for a triangle, 6 for a hexagon, etc..)
|
||||
:param bbox: A bounding box which inscribes the polygon
|
||||
(e.g. bbox = [(50, 50), (150, 150)])
|
||||
:param n_sides: Number of sides
|
||||
(e.g. n_sides=3 for a triangle, 6 for a hexagon, etc..)
|
||||
:param rotation: Apply an arbitrary rotation to the polygon
|
||||
(e.g. rotation=90, applies a 90 degree rotation)
|
||||
:param outline: Color to use for the outline.
|
||||
|
|
|
@ -242,10 +242,10 @@ class ImageDraw:
|
|||
if ink is not None and ink != fill:
|
||||
self.draw.draw_polygon(xy, ink, 0)
|
||||
|
||||
def regular_polygon(self, *, nb_sides, b_box, rotation=0, fill=None, outline=None):
|
||||
def regular_polygon(self, bbox, n_sides, rotation=0, fill=None, outline=None):
|
||||
"""Draw a regular polygon."""
|
||||
xy = _compute_regular_polygon_vertices(
|
||||
nb_sides=nb_sides, b_box=b_box, rotation=rotation
|
||||
n_sides=n_sides, bbox=bbox, rotation=rotation
|
||||
)
|
||||
self.polygon(xy, fill=fill, outline=outline)
|
||||
|
||||
|
@ -562,14 +562,14 @@ def floodfill(image, xy, value, border=None, thresh=0):
|
|||
edge = new_edge
|
||||
|
||||
|
||||
def _compute_regular_polygon_vertices(*, nb_sides, b_box, rotation):
|
||||
def _compute_regular_polygon_vertices(*, n_sides, bbox, rotation):
|
||||
"""
|
||||
Generate a list of vertices for a 2D regular polygon.
|
||||
|
||||
:param nb_sides: Number of sides
|
||||
(e.g. nb_sides = 3 for a triangle, 6 for a hexagon, etc..)
|
||||
:param b_box: A bounding box which inscribes the polygon
|
||||
(e.g. b_box = [(50, 50), (150, 150)])
|
||||
:param n_sides: Number of sides
|
||||
(e.g. n_sides = 3 for a triangle, 6 for a hexagon, etc..)
|
||||
:param bbox: A bounding box (square) which inscribes the polygon
|
||||
(e.g. bbox = [(50, 50), (150, 150)] or [50, 50, 150, 150])
|
||||
:param rotation: Apply an arbitrary rotation to the polygon
|
||||
(e.g. rotation=90, applies a 90 degree rotation)
|
||||
:return: List of regular polygon vertices
|
||||
|
@ -601,36 +601,48 @@ def _compute_regular_polygon_vertices(*, nb_sides, b_box, rotation):
|
|||
Y = −xsin(φ) + ycos(φ) + centroid_y
|
||||
"""
|
||||
# 1. Error Handling
|
||||
# 1.1 Check `nb_sides` has an appropriate value
|
||||
if not isinstance(nb_sides, int):
|
||||
raise TypeError("nb_sides should be an int")
|
||||
if nb_sides < 3:
|
||||
raise ValueError("nb_sides should be an int > 2")
|
||||
# 1.1 Check `n_sides` has an appropriate value
|
||||
if not isinstance(n_sides, int):
|
||||
raise TypeError("n_sides should be an int")
|
||||
if n_sides < 3:
|
||||
raise ValueError("n_sides should be an int > 2")
|
||||
|
||||
# 1.2 Check `b_box` has an appropriate value
|
||||
if not isinstance(b_box, (list, tuple)):
|
||||
raise TypeError("b_box should be a list/tuple")
|
||||
if not len(b_box) == 2:
|
||||
# 1.2 Check `bbox` has an appropriate value
|
||||
if not isinstance(bbox, (list, tuple)):
|
||||
raise TypeError("bbox should be a list/tuple")
|
||||
|
||||
if not len(bbox) == 2 and not len(bbox) == 4:
|
||||
raise ValueError(
|
||||
"b_box should have 2 items (top-left & bottom-right coordinates)"
|
||||
"bbox should have the following format "
|
||||
"[(x0, y0), (x1, y1)] or [x0, y0, x1, y1]"
|
||||
)
|
||||
# Flatten bbox if [(x0, y0), (x1, y1)] format used.
|
||||
if len(bbox) == 2:
|
||||
bbox = [pt for corner in bbox for pt in corner]
|
||||
|
||||
if not len(bbox) == 4:
|
||||
raise ValueError(
|
||||
"bbox should contain top-left and bottom-right coordinates (2D)"
|
||||
)
|
||||
|
||||
b_box_pts = [pt for corner in b_box for pt in corner]
|
||||
if not all(isinstance(i, (int, float)) for i in b_box_pts):
|
||||
raise ValueError("b_box should only contain numeric data")
|
||||
if not all(isinstance(i, (int, float)) for i in bbox):
|
||||
raise ValueError("bbox should only contain numeric data")
|
||||
|
||||
if b_box[1][1] <= b_box[0][1] or b_box[1][0] <= b_box[0][0]:
|
||||
if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]:
|
||||
raise ValueError(
|
||||
"b_box: Bottom-right coordinate should be larger than top-left coordinate"
|
||||
"bbox: Bottom-right coordinate should be larger than top-left coordinate"
|
||||
)
|
||||
|
||||
if not bbox[2] - bbox[0] == bbox[3] - bbox[1]:
|
||||
raise ValueError("bbox should be a square")
|
||||
|
||||
# 1.3 Check `rotation` has an appropriate value
|
||||
if not isinstance(rotation, (int, float)):
|
||||
raise ValueError("rotation should be an int or float")
|
||||
|
||||
# 2. Define Helper Functions
|
||||
def _get_centroid(*, b_box):
|
||||
return (b_box[1][0] + b_box[0][0]) * 0.5, (b_box[1][1] + b_box[0][1]) * 0.5
|
||||
def _get_centroid(*, bbox):
|
||||
return (bbox[2] + bbox[0]) * 0.5, (bbox[3] + bbox[1]) * 0.5
|
||||
|
||||
def _apply_rotation(*, point, degrees, centroid):
|
||||
return (
|
||||
|
@ -648,8 +660,8 @@ def _compute_regular_polygon_vertices(*, nb_sides, b_box, rotation):
|
|||
),
|
||||
)
|
||||
|
||||
def _get_theta(*, nb_sides):
|
||||
return 0.5 * (360 / nb_sides)
|
||||
def _get_theta(*, n_sides):
|
||||
return 0.5 * (360 / n_sides)
|
||||
|
||||
def _get_polygon_radius(*, side_length, theta):
|
||||
return (0.5 * side_length) / math.sin(math.radians(theta))
|
||||
|
@ -658,16 +670,16 @@ def _compute_regular_polygon_vertices(*, nb_sides, b_box, rotation):
|
|||
start_point = [polygon_radius, 0]
|
||||
return _apply_rotation(point=start_point, degrees=angle, centroid=centroid)
|
||||
|
||||
def _get_side_length(*, b_box, theta):
|
||||
h = b_box[1][1] - b_box[0][1]
|
||||
def _get_side_length(*, bbox, theta):
|
||||
h = bbox[3] - bbox[1]
|
||||
return h * math.sin(math.radians(theta))
|
||||
|
||||
def _get_angles(*, nb_sides, rotation):
|
||||
def _get_angles(*, n_sides, rotation):
|
||||
angles = []
|
||||
degrees = 360 / nb_sides
|
||||
degrees = 360 / n_sides
|
||||
# Start with the bottom left polygon vertex
|
||||
current_angle = (270 - 0.5 * degrees) + rotation
|
||||
for _ in range(0, nb_sides):
|
||||
for _ in range(0, n_sides):
|
||||
angles.append(current_angle)
|
||||
current_angle += degrees
|
||||
if current_angle > 360:
|
||||
|
@ -676,11 +688,11 @@ def _compute_regular_polygon_vertices(*, nb_sides, b_box, rotation):
|
|||
|
||||
# 3. Variable Declarations
|
||||
vertices = []
|
||||
theta = _get_theta(nb_sides=nb_sides)
|
||||
side_length = _get_side_length(theta=theta, b_box=b_box)
|
||||
centroid = _get_centroid(b_box=b_box)
|
||||
theta = _get_theta(n_sides=n_sides)
|
||||
side_length = _get_side_length(theta=theta, bbox=bbox)
|
||||
centroid = _get_centroid(bbox=bbox)
|
||||
polygon_radius = _get_polygon_radius(side_length=side_length, theta=theta)
|
||||
angles = _get_angles(nb_sides=nb_sides, rotation=rotation)
|
||||
angles = _get_angles(n_sides=n_sides, rotation=rotation)
|
||||
|
||||
# 4. Compute Vertices
|
||||
for angle in angles:
|
||||
|
|
Loading…
Reference in New Issue
Block a user