Repository: python-pillow/Pillow

Issue:
Polyline joints are ugly
When drawing a Polyline using PIL.ImageDraw.Draw.line(xy, fill=None, width=0), the segment joints are two rectangles that join together in the center of their widths. This leaves an ugly gap. To make it look pretty it could either extend the rectangle sides until they meet, or draw a radiused joint. Extending the sides could lead to very long extensions on highly acute angles, but a radiused joint works well in every case I can think of.

I suggest changing the function to:

`PIL.ImageDraw.Draw.line(xy, fill=None, width=0, radiused=False)`

and then for each point in xy[1:-2] draw an ellipse centered at the point and whose width and height are the same as the line width. Here is a snippet of how I did it externally

```
draw.line(plot,'black', Weight)
for joint in plot[1:-2]:
    draw.ellipse((joint[0]-Weight/2,joint[1]-Weight/2,
                  joint[0]+Weight/2,joint[1]+Weight/2),'black')
```




Discussion:
Feel free to send a PR for this. 
Will do. I need to get a proper dev environment (I'm running Windows) and add some unittests, then I'll submit a PR.
Please see PR https://github.com/python-pillow/Pillow/pull/3250. 

Resolving patch:
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -30,6 +30,7 @@
 # See the README file for information on usage and redistribution.
 #
 
+import math
 import numbers
 
 from . import Image, ImageColor
@@ -149,11 +150,64 @@ def ellipse(self, xy, fill=None, outline=None):
         if ink is not None and ink != fill:
             self.draw.draw_ellipse(xy, ink, 0)
 
-    def line(self, xy, fill=None, width=0):
+    def line(self, xy, fill=None, width=0, joint=None):
         """Draw a line, or a connected sequence of line segments."""
-        ink, fill = self._getink(fill)
+        ink = self._getink(fill)[0]
         if ink is not None:
             self.draw.draw_lines(xy, ink, width)
+            if joint == "curve" and width > 4:
+                for i in range(1, len(xy)-1):
+                    point = xy[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]))
+                    ]
+                    if angles[0] == angles[1]:
+                        # This is a straight line, so no joint is required
+                        continue
+
+                    def coord_at_angle(coord, angle):
+                        x, y = coord
+                        angle -= 90
+                        distance = width/2 - 1
+                        return tuple([
+                            p +
+                            (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
+                            for p, p_d in
+                            ((x, distance * math.cos(math.radians(angle))),
+                             (y, distance * math.sin(math.radians(angle))))
+                        ])
+                    flipped = ((angles[1] > angles[0] and
+                                angles[1] - 180 > angles[0]) or
+                               (angles[1] < angles[0] and
+                                angles[1] + 180 > angles[0]))
+                    coords = [
+                        (point[0] - width/2 + 1, point[1] - width/2 + 1),
+                        (point[0] + width/2 - 1, point[1] + width/2 - 1)
+                    ]
+                    if flipped:
+                        start, end = (angles[1] + 90, angles[0] + 90)
+                    else:
+                        start, end = (angles[0] - 90, angles[1] - 90)
+                    self.pieslice(coords, start - 90, end - 90, fill)
+
+                    if width > 8:
+                        # Cover potential gaps between the line and the joint
+                        if flipped:
+                            gapCoords = [
+                                coord_at_angle(point, angles[0]+90),
+                                point,
+                                coord_at_angle(point, angles[1]+90)
+                            ]
+                        else:
+                            gapCoords = [
+                                coord_at_angle(point, angles[0]-90),
+                                point,
+                                coord_at_angle(point, angles[1]-90)
+                            ]
+                        self.line(gapCoords, fill, width=3)
 
     def shape(self, shape, fill=None, outline=None):
         """(Experimental) Draw a shape."""


Test patch:
diff --git a/Tests/images/imagedraw_line_joint_curve.png b/Tests/images/imagedraw_line_joint_curve.png
new file mode 100644
Binary files /dev/null and b/Tests/images/imagedraw_line_joint_curve.png differ
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -568,6 +568,20 @@ def test_wide_line_dot(self):
         # Assert
         self.assert_image_similar(im, Image.open(expected), 1)
 
+    def test_line_joint(self):
+        im = Image.new("RGB", (500, 325))
+        draw = ImageDraw.Draw(im)
+        expected = "Tests/images/imagedraw_line_joint_curve.png"
+
+        # Act
+        xy = [(400, 280), (380, 280), (450, 280), (440, 120), (350, 200),
+              (310, 280), (300, 280), (250, 280), (250, 200), (150, 200),
+              (150, 260), (50, 200), (150, 50), (250, 100)]
+        draw.line(xy, GRAY, 50, "curve")
+
+        # Assert
+        self.assert_image_similar(im, Image.open(expected), 3)
+
     def test_textsize_empty_string(self):
         # https://github.com/python-pillow/Pillow/issues/2783
         # Arrange