diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index df81b3c46..78fdec9c1 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1107,7 +1107,8 @@ def test_draw_regular_polygon(n_sides, rotation, polygon_name): else f"{filename_base}_rotate_{rotation}.png" ) draw = ImageDraw.Draw(im) - draw.regular_polygon([W // 2, H // 2, 25], n_sides, rotation=rotation, fill="red") + bounding_circle = ((W // 2, H // 2), 25) + draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") assert_image_equal(im, Image.open(filename)) @@ -1140,39 +1141,46 @@ def test_draw_regular_polygon(n_sides, rotation, polygon_name): ], ) def test_compute_regular_polygon_vertices(n_sides, expected_vertices): - vertices = ImageDraw._compute_regular_polygon_vertices( - [W // 2, H // 2, 25], n_sides, 0 - ) + bounding_circle = (W // 2, H // 2, 25) + vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) assert vertices == expected_vertices @pytest.mark.parametrize( - "n_sides, b_circle, rotation, expected_error, error_message", + "n_sides, bounding_circle, rotation, expected_error, error_message", [ - (None, [50, 50, 25], 0, TypeError, "n_sides should be an int"), - (1, [50, 50, 25], 0, ValueError, "n_sides should be an int > 2"), - (3, 50, 0, TypeError, "b_circle should be a list/tuple"), + (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), + (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), + (3, 50, 0, TypeError, "bounding_circle should be a tuple"), ( 3, - [50, 50, 100, 100], + (50, 50, 100, 100), 0, ValueError, - "b_circle should contain 2D coordinates and a radius (e.g. [x0, y0, r])", + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )", ), ( 3, - [50, 50, "25"], + (50, 50, "25"), 0, ValueError, - "b_circle should only contain numeric data", + "bounding_circle should only contain numeric data", ), - (3, [50, 50, 0], 0, ValueError, "b_circle radius should be > 0",), - (3, [50, 50, 25], "0", ValueError, "rotation should be an int or float",), + ( + 3, + ((50, 50, 50), 25), + 0, + ValueError, + "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", + ), + (3, (50, 50, 0), 0, ValueError, "bounding_circle radius should be > 0",), + (3, (50, 50, 25), "0", ValueError, "rotation should be an int or float",), ], ) def test_compute_regular_polygon_vertices_input_error_handling( - n_sides, b_circle, rotation, expected_error, error_message + n_sides, bounding_circle, rotation, expected_error, error_message ): with pytest.raises(expected_error) as e: - ImageDraw._compute_regular_polygon_vertices(b_circle, n_sides, rotation) + ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) assert str(e.value) == error_message diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 3f740110e..547172c39 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -255,17 +255,19 @@ Methods :param fill: Color to use for the fill. -.. py:method:: ImageDraw.regular_polygon(b_circle, n_sides, rotation=0, fill=None, outline=None) +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) - Draws a regular polygon inscribed in ``b_circle``, + Draws a regular polygon inscribed in ``bounding_circle``, with ``n_sides``, and rotation of ``rotation`` degrees. - :param b_circle: A bounding circle which inscribes the polygon - (e.g. b_circle=[50, 50, 25]). + :param bounding_circle: The bounding circle is a tuple defined + by a point and radius. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``). + The polygon is inscribed in this circle. :param n_sides: Number of sides - (e.g. n_sides=3 for a triangle, 6 for a hexagon). + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon). :param rotation: Apply an arbitrary rotation to the polygon - (e.g. rotation=90, applies a 90 degree rotation). + (e.g. ``rotation=90``, applies a 90 degree rotation). :param fill: Color to use for the fill. :param outline: Color to use for the outline. diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 1ef1fd4f2..521f6da9e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -242,9 +242,11 @@ class ImageDraw: if ink is not None and ink != fill: self.draw.draw_polygon(xy, ink, 0) - def regular_polygon(self, b_circle, n_sides, rotation=0, fill=None, outline=None): + def regular_polygon( + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + ): """Draw a regular polygon.""" - xy = _compute_regular_polygon_vertices(b_circle, n_sides, rotation) + xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline) def rectangle(self, xy, fill=None, outline=None, width=1): @@ -560,25 +562,26 @@ def floodfill(image, xy, value, border=None, thresh=0): edge = new_edge -def _compute_regular_polygon_vertices(b_circle, n_sides, rotation): +def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): """ Generate a list of vertices for a 2D regular polygon. - :param b_circle: A bounding circle which inscribes the polygon - (e.g. b_circle = [x0, y0, r]) + :param bounding_circle: The bounding circle is a tuple defined + by a point and radius. The polygon is inscribed in this circle. + (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) :param n_sides: Number of sides - (e.g. n_sides = 3 for a triangle, 6 for a hexagon) + (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) :param rotation: Apply an arbitrary rotation to the polygon - (e.g. rotation=90, applies a 90 degree rotation) + (e.g. ``rotation=90``, applies a 90 degree rotation) :return: List of regular polygon vertices - (e.g. [(25, 50), (50, 50), (50, 25), (25, 25)]) + (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) How are the vertices computed? 1. Compute the following variables - theta: Angle between the apothem & the nearest polygon vertex - side_length: Length of each polygon edge - - centroid: Center of bounding circle (1st, 2nd elements of b_circle) - - polygon_radius: Polygon radius (3rd element of b_circle) + - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) + - polygon_radius: Polygon radius (last element of bounding_circle) - angles: Location of each polygon vertex in polar grid (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) @@ -605,20 +608,30 @@ def _compute_regular_polygon_vertices(b_circle, n_sides, rotation): if n_sides < 3: raise ValueError("n_sides should be an int > 2") - # 1.2 Check `b_circle` has an appropriate value - if not isinstance(b_circle, (list, tuple)): - raise TypeError("b_circle should be a list/tuple") + # 1.2 Check `bounding_circle` has an appropriate value + if not isinstance(bounding_circle, (list, tuple)): + raise TypeError("bounding_circle should be a tuple") - if not len(b_circle) == 3: + if len(bounding_circle) == 3: + *centroid, polygon_radius = bounding_circle + elif len(bounding_circle) == 2: + centroid, polygon_radius = bounding_circle + else: raise ValueError( - "b_circle should contain 2D coordinates and a radius (e.g. [x0, y0, r])" + "bounding_circle should contain 2D coordinates " + "and a radius (e.g. (x, y, r) or ((x, y), r) )" ) - if not all(isinstance(i, (int, float)) for i in b_circle): - raise ValueError("b_circle should only contain numeric data") + if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): + raise ValueError("bounding_circle should only contain numeric data") - if b_circle[-1] <= 0: - raise ValueError("b_circle radius should be > 0") + if not len(centroid) == 2: + raise ValueError( + "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + ) + + if polygon_radius <= 0: + raise ValueError("bounding_circle radius should be > 0") # 1.3 Check `rotation` has an appropriate value if not isinstance(rotation, (int, float)): @@ -641,7 +654,7 @@ def _compute_regular_polygon_vertices(b_circle, n_sides, rotation): ), ) - def _compute_polygon_vertex(centroid, angle, polygon_radius): + def _compute_polygon_vertex(centroid, polygon_radius, angle): start_point = [polygon_radius, 0] return _apply_rotation(start_point, angle, centroid) @@ -658,14 +671,12 @@ def _compute_regular_polygon_vertices(b_circle, n_sides, rotation): return angles # 3. Variable Declarations - vertices = [] - *centroid, polygon_radius = b_circle angles = _get_angles(n_sides, rotation) # 4. Compute Vertices - for angle in angles: - vertices.append(_compute_polygon_vertex(centroid, angle, polygon_radius)) - return vertices + return [ + _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles + ] def _color_diff(color1, color2):