Search code examples
pyqtglyphqpainterpath

Drawing Bezier curve with multiple off curve points in PyQt


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.


Solution

  • 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]