From df9329f9f0509f2e5544702726236b8720577c17 Mon Sep 17 00:00:00 2001 From: Tommy C Date: Thu, 20 Aug 2020 20:46:11 +0100 Subject: [PATCH] Replace `bbox` with `b_circle`, kwargs with args + minor tweaks Summary of changes: - `ImageDraw.regular_polygon` now accepts a bounding circle which inscribes the polygon. A bounding circle is defined by a center point (x0, y0) and a radius. A bounding box is no longer accepted. - All keyword args have been replaced with positional args. Misc - Test image file renaming, minor variable name changes --- Tests/images/imagedraw_regular_octagon.png | Bin 0 -> 491 bytes ...olygon__octagon_with_0_degree_rotation.png | Bin 635 -> 0 bytes ...polygon__square_with_0_degree_rotation.png | Bin 327 -> 0 bytes ...olygon__square_with_45_degree_rotation.png | Bin 844 -> 0 bytes Tests/images/imagedraw_square.png | Bin 0 -> 327 bytes Tests/images/imagedraw_square_rotate_45.png | Bin 0 -> 587 bytes Tests/test_imagedraw.py | 95 +++++++----------- docs/reference/ImageDraw.rst | 16 +-- src/PIL/ImageDraw.py | 86 +++++----------- 9 files changed, 69 insertions(+), 128 deletions(-) create mode 100644 Tests/images/imagedraw_regular_octagon.png delete mode 100644 Tests/images/imagedraw_regular_polygon__octagon_with_0_degree_rotation.png delete mode 100644 Tests/images/imagedraw_regular_polygon__square_with_0_degree_rotation.png delete mode 100644 Tests/images/imagedraw_regular_polygon__square_with_45_degree_rotation.png create mode 100644 Tests/images/imagedraw_square.png create mode 100644 Tests/images/imagedraw_square_rotate_45.png diff --git a/Tests/images/imagedraw_regular_octagon.png b/Tests/images/imagedraw_regular_octagon.png new file mode 100644 index 0000000000000000000000000000000000000000..7f215dc08bff7a57f70f98f2c60927133ba623e1 GIT binary patch literal 491 zcmeAS@N?(olHy`uVBq!ia0vp^DIm=oBnT%KB29mZDq^kIaE=#wJ{JwybbJXvTV!khBgL8OaiQ>6r(E2AKv zQjUT{hjY!geW}tZe;NK~lwMZr=qTK}EtlKn*p8^1I~a@DdsaV=_HVw!`S{g~XOk7m zkL~E+-L}LPLlPE_R0mfXGh4nkX%k`ab1qS$N0L9~ z*_%d<9!cSpXJ-~#^hk=QJlo2yY?ziaVdiz2zN0f{R^IuU?rU%Om-A;SF>b?DQ_H}| XCVzZFyJQnE5*a*Q{an^LB{Ts537fn> literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_regular_polygon__octagon_with_0_degree_rotation.png b/Tests/images/imagedraw_regular_polygon__octagon_with_0_degree_rotation.png deleted file mode 100644 index 6e529a1a47754d45e09064db8f369748fc0430c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 635 zcmeAS@N?(olHy`uVBq!ia0vp^DIm zz1;X4gCwJ}Xx+7p2@?&17N+;HND3;8mVMKhFwrnb(b%QoOvA*Y`A4Nw)Ox18_L-+( zoV4Ul+GnPjlFCnw7PFt=lFo6+)m|`X-}AH7^G&sfr0UH@Mpa! z#{(AEw@eljFnG86p06&4#QV^{>9GA`ct=I+y`hKq66_=+74 zY5zRNi;^+|C?bF@+>XCE5|! z%sr;pf8JctJ~zI$y+P=D=Jj&>O%5O57ajNC@9gmLb=mg({Z||}zAs#ES)baNbp5qn z@&7lhOZTpkomRugxD@E8sdWY{OZP0Ad$lHrU3LGX*sFF{j8FHjlHF=^n(67jS9iDA z#jre$x$LJi?_1NSm6hUQ{r4RWZ#6#sJz;9#nY8V7j$4(wUY?Q5=LvT*oPYJT=sPYA zJ>#ll8JS?~#kvF`jJ@GID;hcg@7WL+9jb1M^&_TrmE= zW2c3X*N@2RSXEiZrKcs!{~ezg7PC!^TVUfqMwYjh$Fu(g>;xt%22WQ%mvv4FO#r)+ B0Z;${ diff --git a/Tests/images/imagedraw_regular_polygon__square_with_0_degree_rotation.png b/Tests/images/imagedraw_regular_polygon__square_with_0_degree_rotation.png deleted file mode 100644 index 0c94dd42e59c11cf64810a5a54245f7e7eb48694..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 327 zcmeAS@N?(olHy`uVBq!ia0vp^DImt{`_XN;S^H#=ut3iI>Ld$SkoKX{;2$O>WMZil9bP0l+XkK9Ia@r diff --git a/Tests/images/imagedraw_regular_polygon__square_with_45_degree_rotation.png b/Tests/images/imagedraw_regular_polygon__square_with_45_degree_rotation.png deleted file mode 100644 index aef8dba52c8da50fec657c7fda8af0b52cab710f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 844 zcmV-S1GD^zP)3U zD}a0=NTaBO?3FKzq6~6Yx+ID!NN?G4D2gDXB}<{Gfm|(D21N;Ewp0le6%ea3=}{Cw ztV^UuVF&Rlj~0a)#Je;~6jqR^vglA4L842dLSX~RDu)Jz2_(Ce?AHgt142;-2T49F zFs`M$w-J!Sm`hiGyQlhYo1KnlY)hkc%b6oyZBdp!av4Bu?_y9ZJjG5P9w3#2e& z^Vat#NMYpUulMKHH!CT50slY>gCnme3WF+7EO0E|N@+i`3WXz&|9`0$N^Ty&_n*Cm zl9RvQxBf9Kl-RuWy?5!PP-61c^U=k{Lh;RJzh{@<3&kh9z4p98RVcPO?6dbR+(NO* zX^$f}ITXrVy80Wv4X9AArMtH)H_8>tScdw#dMjz6^kulGnVYQ(~H;8^j%TR_Vze9;BF|U`+6O%aTgTxJv|Sf zSsle?Kkwt2Rz-2!E8+mN)lgjbi8|tM6%_Y9A`ih-M+uof`j}W%l(4xo4pLS_37t3V zsBsmP@HsON<7Y=nnJ@d}Dp*m{=E?>E_)t>kVE_SmP}1jM0Ri&M$bNkQTp&PRDHIq0 z9|#a%4h;st2?E5GM1=wHf&l(y(P04GAb?+Klo$X%2w-0xEe4Z-UbH_2FG(Z z>QDdL#-vuboYBL~#`3&NQLg@k^-T#}O`06b6lRdm*tuPNhU8n4(x^%dCvj5Ea6EP)a!g{xU7f;ro?`{-obSl`(T5_rKKH2{G*aDTtKc!bVrie^P z?_KHc$x!*NBTK~kMC{|tz%vTv{mWc7Hu>}}bvA7B>0jcsu`#E2v7=#QPXDIAH3yg% zNiZLjWt;F-P%dh3y7ZB@!mVZ3)jC8UXFc7gD`0)>>W{h73Yrq(i@*CdFG-B)y=%)k zsjJYm=)Z`E)v>E@*C%s5Nqu2iUjF;_j6YMS_8ArZ|04f2WXi_f=J&oWTiA1N_t%34 ztf_y_on7zuMnn61ZvD;-*T~)3(kwL^tG`P)9BAHD*UrlF?f8v4y9Z2-djmxz9xyll zEHsgLz|y#LVu-{8*2dq*FC{iev%Hh`?PRPIsQ6bA#dCt8@pPm96A6Q-tDnm{r-UW|^8WOV literal 0 HcmV?d00001 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 7a530aa1d..df81b3c46 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1095,107 +1095,84 @@ def test_same_color_outline(): @pytest.mark.parametrize( - "n_polygon_sides, rotation, polygon_name", - [(4, 0, "square"), (8, 0, "octagon"), (4, 45, "square")], + "n_sides, rotation, polygon_name", + [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], ) -def test_draw_regular_polygon(n_polygon_sides, rotation, polygon_name): +def test_draw_regular_polygon(n_sides, rotation, polygon_name): im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) + filename_base = f"Tests/images/imagedraw_{polygon_name}" filename = ( - f"Tests/images/imagedraw_regular_polygon__{polygon_name}" - f"_with_{rotation}_degree_rotation.png" + f"{filename_base}.png" + if rotation == 0 + else f"{filename_base}_rotate_{rotation}.png" ) draw = ImageDraw.Draw(im) - draw.regular_polygon( - [(0, 0), (W, H)], n_polygon_sides, rotation=rotation, fill="red" - ) + draw.regular_polygon([W // 2, H // 2, 25], n_sides, rotation=rotation, fill="red") assert_image_equal(im, Image.open(filename)) @pytest.mark.parametrize( - "n_polygon_sides, expected_vertices", + "n_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)]), + (3, [(28.35, 62.5), (71.65, 62.5), (50.0, 25.0)]), + (4, [(32.32, 67.68), (67.68, 67.68), (67.68, 32.32), (32.32, 32.32)]), ( 5, [ - (20.61, 90.45), - (79.39, 90.45), - (97.55, 34.55), - (50.0, 0.0), - (2.45, 34.55), + (35.31, 70.23), + (64.69, 70.23), + (73.78, 42.27), + (50.0, 25.0), + (26.22, 42.27), ], ), ( 6, [ - (25.0, 93.3), - (75.0, 93.3), - (100.0, 50.0), - (75.0, 6.7), - (25.0, 6.7), - (0.0, 50.0), + (37.5, 71.65), + (62.5, 71.65), + (75.0, 50.0), + (62.5, 28.35), + (37.5, 28.35), + (25.0, 50.0), ], ), ], ) -def test_compute_regular_polygon_vertices(n_polygon_sides, expected_vertices): +def test_compute_regular_polygon_vertices(n_sides, expected_vertices): vertices = ImageDraw._compute_regular_polygon_vertices( - n_sides=n_polygon_sides, bbox=[(0, 0), (100, 100)], rotation=0 + [W // 2, H // 2, 25], n_sides, 0 ) assert vertices == expected_vertices @pytest.mark.parametrize( - "n_polygon_sides, bounding_box, rotation, expected_error, error_message", + "n_sides, b_circle, rotation, expected_error, error_message", [ - (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"), + (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"), ( 3, - [(0, 0), (100,), (100,)], + [50, 50, 100, 100], 0, ValueError, - "bbox should have the following format " - "[(x0, y0), (x1, y1)] or [x0, y0, x1, y1]", + "b_circle should contain 2D coordinates and a radius (e.g. [x0, y0, r])", ), ( 3, - [(50, 50), (100,)], + [50, 50, "25"], 0, ValueError, - "bbox should contain top-left and bottom-right coordinates (2D)", - ), - ( - 3, - [(50, 50), (0, None)], - 0, - ValueError, - "bbox should only contain numeric data", - ), - ( - 3, - [(50, 50), (0, 0)], - 0, - ValueError, - "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)], - "0", - ValueError, - "rotation should be an int or float", + "b_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",), ], ) def test_compute_regular_polygon_vertices_input_error_handling( - n_polygon_sides, bounding_box, rotation, expected_error, error_message + n_sides, b_circle, rotation, expected_error, error_message ): with pytest.raises(expected_error) as e: - ImageDraw._compute_regular_polygon_vertices( - n_sides=n_polygon_sides, bbox=bounding_box, rotation=rotation - ) + ImageDraw._compute_regular_polygon_vertices(b_circle, n_sides, rotation) assert str(e.value) == error_message diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 6bfe4cdc0..3f740110e 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -255,19 +255,19 @@ Methods :param fill: Color to use for the fill. -.. py:method:: ImageDraw.regular_polygon(bbox, n_sides, rotation=0, fill=None, outline=None) +.. py:method:: ImageDraw.regular_polygon(b_circle, n_sides, rotation=0, fill=None, outline=None) - Draws a regular polygon inscribed in ``bbox``, - with ``n_sides``, and rotation of ``rotation`` degrees + Draws a regular polygon inscribed in ``b_circle``, + with ``n_sides``, and rotation of ``rotation`` degrees. - :param bbox: A bounding box which inscribes the polygon - (e.g. bbox = [(50, 50), (150, 150)]) + :param b_circle: A bounding circle which inscribes the polygon + (e.g. b_circle=[50, 50, 25]). :param n_sides: Number of sides - (e.g. n_sides=3 for a triangle, 6 for a hexagon, etc..) + (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) - :param outline: Color to use for the outline. + (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. .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index a64325ac6..1ef1fd4f2 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -242,12 +242,10 @@ class ImageDraw: if ink is not None and ink != fill: self.draw.draw_polygon(xy, ink, 0) - def regular_polygon(self, bbox, n_sides, rotation=0, fill=None, outline=None): + def regular_polygon(self, b_circle, n_sides, rotation=0, fill=None, outline=None): """Draw a regular polygon.""" - xy = _compute_regular_polygon_vertices( - n_sides=n_sides, bbox=bbox, rotation=rotation - ) - self.polygon(xy, fill=fill, outline=outline) + xy = _compute_regular_polygon_vertices(b_circle, n_sides, rotation) + self.polygon(xy, fill, outline) def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" @@ -562,14 +560,14 @@ def floodfill(image, xy, value, border=None, thresh=0): edge = new_edge -def _compute_regular_polygon_vertices(*, n_sides, bbox, rotation): +def _compute_regular_polygon_vertices(b_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 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]) + (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) :return: List of regular polygon vertices @@ -579,8 +577,8 @@ def _compute_regular_polygon_vertices(*, n_sides, bbox, rotation): 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 box - - polygon_radius: Distance between centroid and each polygon vertex + - centroid: Center of bounding circle (1st, 2nd elements of b_circle) + - polygon_radius: Polygon radius (3rd element of b_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]) @@ -607,44 +605,27 @@ def _compute_regular_polygon_vertices(*, n_sides, bbox, rotation): if n_sides < 3: raise ValueError("n_sides should be an int > 2") - # 1.2 Check `bbox` has an appropriate value - if not isinstance(bbox, (list, tuple)): - raise TypeError("bbox should be a list/tuple") + # 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") - if not len(bbox) == 2 and not len(bbox) == 4: + if not len(b_circle) == 3: raise ValueError( - "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_circle should contain 2D coordinates and a radius (e.g. [x0, y0, r])" ) - if not all(isinstance(i, (int, float)) for i in bbox): - raise ValueError("bbox should only contain numeric data") + if not all(isinstance(i, (int, float)) for i in b_circle): + raise ValueError("b_circle should only contain numeric data") - if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: - raise ValueError( - "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") + if b_circle[-1] <= 0: + raise ValueError("b_circle radius should be > 0") # 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(*, bbox): - return (bbox[2] + bbox[0]) * 0.5, (bbox[3] + bbox[1]) * 0.5 - - def _apply_rotation(*, point, degrees, centroid): + def _apply_rotation(point, degrees, centroid): return ( round( point[0] * math.cos(math.radians(360 - degrees)) @@ -660,21 +641,11 @@ def _compute_regular_polygon_vertices(*, n_sides, bbox, rotation): ), ) - 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)) - - def _compute_polygon_vertex(*, angle, centroid, polygon_radius): + def _compute_polygon_vertex(centroid, angle, polygon_radius): start_point = [polygon_radius, 0] - return _apply_rotation(point=start_point, degrees=angle, centroid=centroid) + return _apply_rotation(start_point, angle, centroid) - def _get_side_length(*, bbox, theta): - h = bbox[3] - bbox[1] - return h * math.sin(math.radians(theta)) - - def _get_angles(*, n_sides, rotation): + def _get_angles(n_sides, rotation): angles = [] degrees = 360 / n_sides # Start with the bottom left polygon vertex @@ -688,19 +659,12 @@ def _compute_regular_polygon_vertices(*, n_sides, bbox, rotation): # 3. Variable Declarations vertices = [] - 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(n_sides=n_sides, rotation=rotation) + *centroid, polygon_radius = b_circle + angles = _get_angles(n_sides, rotation) # 4. Compute Vertices for angle in angles: - vertices.append( - _compute_polygon_vertex( - centroid=centroid, angle=angle, polygon_radius=polygon_radius - ) - ) + vertices.append(_compute_polygon_vertex(centroid, angle, polygon_radius)) return vertices