Drawing Bezier curve with multiple off curve points in PyQt

310 views Asked by At

I would like to draw a TrueType Font glyph with PyQt5 QPainterPath. Example glyph fragment: (data from Fonttools ttx )

<pt x="115" y="255" on="1"/>
<pt x="71" y="255" on="0"/>
<pt x="64" y="244" on="0"/>
<pt x="53" y="213" on="0"/>
<pt x="44" y="180" on="0"/>
<pt x="39" y="166" on="1"/> 

on=0 means a control point and on=1 means a start/end point I'm assuming this would not use (QPainterPath) quadTo or cubicTo as it is a higher order curve.

1

There are 1 answers

2
musicamante On BEST ANSWER

True type fonts actually use only quadratic Bézier curves. This makes sense, they are pretty simple curves that don't require a lot of computation, which is good for performance when you have to potentially draw hundreds or thousands of curves even for a simple paragraph.

After realizing this, I found out strange that you have a curve with 4 control points, but then I did a bit of research and found out this interesting answer.

In reality, the TrueType format allows grouping quadratic curves that always share each start or end point at the middle of each control point.

So, starting with your list:

Start  <pt x="115" y="255" on="1"/>
C1     <pt x="71" y="255" on="0"/>
C2     <pt x="64" y="244" on="0"/>
C3     <pt x="53" y="213" on="0"/>
C4     <pt x="44" y="180" on="0"/>
End    <pt x="39" y="166" on="1"/> 

We have 6 points, but there are 4 curves, and the intermediate points between the 4 control points are the remaining start/end points that exist on the curve:

start control end
Start C1 (C2-C1)/2
(C2-C1)/2 C2 (C3-C2)/2
(C3-C2)/2 C3 (C4-C3)/2
(C4-C3)/2 C4 End

To compute all that, we can cycle through the points and store a reference to the previous, and whenever we have a control point or an on-curve point after them, we add a new quadratic curve to the path.

start control end
115 x 255 71 x 255 67.5 x 249.5
67.5 x 249.5 64 x 244 58.5 x 228.5
58.5 x 228.5 53 x 213 48.5 x 106.5
48.5 x 106.5 44 x 180 39 x 166

The following code will create a QPainterPath that corresponds to each <contour> group.

path = QtGui.QPainterPath()
currentCurve = []
started = False
for x, y, onCurve in contour:
    point = QtCore.QPointF(x, y)
    if onCurve:
        if not currentCurve:
            # start of curve
            currentCurve.append(point)
        else:
            # end of curve
            start, cp = currentCurve
            path.quadTo(cp, point)
            currentCurve = []
            started = False
    else:
        if len(currentCurve) == 1:
            # control point
            currentCurve.append(point)
        else:
            start, cp = currentCurve
            # find the midpoint
            end = QtCore.QLineF(cp, point).pointAt(.5)
            if not started:
                # first curve of many
                path.moveTo(start)
                started = True
            path.quadTo(cp, end)
            currentCurve = [end, point]