Produce PDF files, draw polygons with rounded corners

2024/10/10 22:20:44

What's the right tool for the job if I want to write a Python script that produces vector graphics in PDF format? In particular, I need to draw filled polygons with rounded corners (i.e., plane figures that are composed of straight lines and circular arcs).

It seems that matplotlib makes it fairly easy to draw rectangles with rounded corners and general polygons with sharp corners. However, to draw polygons with rounded corners, it seems that I have to first compute a Bézier curve that approximates the shape.

Is there anything more straightforward available? Or is there another library that I can use to compute the Bézier curve that approximates the shape that I want to produce? Ideally, I would simply specify the (location, corner radius) pair for each vertex.

Here is an example: I would like to specify the red polygon (+ the radius of each corner) and the library would output the grey figure:

example

(For convex polygons I could cheat and use a thick pen to draw the outline of the polygon. However, this does not work in the non-convex case.)

Answer

Here's a somewhat hacky matplotlib solution. The main complications are related to using matplotlib Path objects to build a composite Path.

#!/usr/bin/env pythonimport numpy as np
from matplotlib.path import Path
from matplotlib.patches import PathPatch, Polygon
from matplotlib.transforms import Bbox, BboxTransformTodef side(a, b, c):"On which side of line a-b is point c? Returns -1, 0, or 1."return np.sign(np.linalg.det(np.c_[[a,b,c],[1,1,1]]))def center((prev, curr, next), radius):"Find center of arc approximating corner at curr."p0, p1 = prevc0, c1 = currn0, n1 = nextdp = radius * np.hypot(c1 - p1, c0 - p0)dn = radius * np.hypot(c1 - n1, c0 - n0)p = p1 * c0 - p0 * c1n = n1 * c0 - n0 * c1results = \np.linalg.solve([[p1 - c1, c0 - p0],[n1 - c1, c0 - n0]],[[p - dp, p - dp, p + dp, p + dp],[n - dn, n + dn, n - dn, n + dn]])side_n = side(prev, curr, next)side_p = side(next, curr, prev)for r in results.T:if (side(prev, curr, r), side(next, curr, r)) == (side_n, side_p):return rraise ValueError, "Cannot find solution"def proj((prev, curr, next), center):"Project center onto lines prev-curr and next-curr."p0, p1 = prev = np.asarray(prev)c0, c1 = curr = np.asarray(curr)n0, n1 = next = np.asarray(next)pc = curr - prevnc = curr - nextpc2 = np.dot(pc, pc)nc2 = np.dot(nc, nc)return (prev + np.dot(center - prev, pc)/pc2 * pc,next + np.dot(center - next, nc)/nc2 * nc)def rad2deg(angle):return angle * 180.0 / np.pidef angle(center, point):x, y = np.asarray(point) - np.asarray(center)return np.arctan2(y, x)def arc_path(center, start, end):"Return a Path for an arc from start to end around center."# matplotlib arcs always go ccw so we may need to mirrormirror = side(center, start, end) < 0if mirror: start *= [1, -1]center *= [1, -1]end *= [1, -1]return Path.arc(rad2deg(angle(center, start)),rad2deg(angle(center, end))), \mirrordef path(vertices, radii):"Return a Path for a closed rounded polygon."if np.isscalar(radii):radii = np.repeat(radii, len(vertices))else:radii = np.asarray(radii)pv = []pc = []first = Truefor i in range(len(vertices)):if i == 0:seg = (vertices[-1], vertices[0], vertices[1])elif i == len(vertices) - 1:seg = (vertices[-2], vertices[-1], vertices[0])else:seg = vertices[i-1:i+2]r = radii[i]c = center(seg, r)a, b = proj(seg, c)arc, mirror = arc_path(c, a, b)m = [1,1] if not mirror else [1,-1]bb = Bbox([c, c + (r, r)])iter = arc.iter_segments(BboxTransformTo(bb))for v, c in iter:if c == Path.CURVE4:pv.extend([m * v[0:2], m * v[2:4], m * v[4:6]])pc.extend([c, c, c])elif c == Path.MOVETO:pv.append(m * v)if first:pc.append(Path.MOVETO)first = Falseelse:pc.append(Path.LINETO)pv.append([0,0])pc.append(Path.CLOSEPOLY)return Path(pv, pc)if __name__ == '__main__':from matplotlib import pyplotfig = pyplot.figure()ax = fig.add_subplot(111)vertices = [[3,0], [5,2], [10,0], [6,9], [6,5], [3, 5], [0,2]]patch = Polygon(vertices, edgecolor='red', facecolor='None',linewidth=1)ax.add_patch(patch)patch = PathPatch(path(vertices, 0.5), edgecolor='black', facecolor='blue', alpha=0.4,linewidth=2)ax.add_patch(patch)ax.set_xlim(-1, 11)ax.set_ylim(-1, 9)fig.savefig('foo.pdf')

output of script above

https://en.xdnf.cn/q/69845.html

Related Q&A

How can I unit test this Flask app?

I have a Flask app that is using Flask-Restless to serve an API.I have just written some authentication that checksIf the consumers host is recognised The request includes a hash (calculated by encrypt…

How to completely reset Python stdlib logging module in an ipython session?

Id like to make repeated calls to Python scripts using %run in an ipython session, and for each of those scripts to log based on cmdline arguments passed via %run.For example while debugging cmd.py I m…

I think Librosa.effect.split has some problem?

firstly, this function is to remove silence of an audio. here is the official description:https://librosa.github.io/librosa/generated/librosa.effects.split.htmllibrosa.effects.split(y, top_db=10, *karg…

Replace None in list with leftmost non none value

Givena = [None,1,2,3,None,4,None,None]Id likea = [None,1,2,3,3,4,4,4]Currently I have brute forced it with:def replaceNoneWithLeftmost(val):last = Noneret = []for x in val:if x is not None:ret.append(x…

Generating lists/reports with in-line summaries in Django

I am trying to write a view that will generate a report which displays all Items within my Inventory system, and provide summaries at a certain point. This report is purely just an HTML template by the…

How to test if a view is decorated with login_required (Django)

Im doing some (isolated) unit test for a view which is decorated with "login_required". Example:@login_required def my_view(request):return HttpResponse(test)Is it possible to test that the &…

Filter Nested field in Flask Marshmallow

I want to filter the nested field with is_active column as True in Marshmallow 3 Consider following scenario I have 3 tablesusers (id, name) organizations (id, name) organization_user(id, organization_…

Copy signature, forward all arguments from wrapper function

I have two functions in a class, plot() and show(). show(), as convenience method, does nothing else than to add two lines to the code of plot() likedef plot(self,show_this=True,show_that=True,color=k,…

How to fit a line through a 3D pointcloud?

I have a cable I am dropping from moving vehicle onto the ground. Using a camera system I estimate the location where the rope touches the ground in realtime. Movement of the vehicle and inaccuracy in …

Websockets with Django Channels on Heroku

I am trying to deploy my app to heroku. The app has a simple chatting system that uses Websockets and django channels. When I test my app using python manage.py runserver the app behaves just as intend…