0

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.

ajlogo
  • 3
  • 2
  • If by "glyph" you mean a single character, why don't you use [`addText()`](https://doc.qt.io/qt-5/qpainterpath.html#addText)? – musicamante Jan 09 '22 at 11:08
  • I would then like to add handles and change the glyph. Looking into: JDesigner https://github.com/jakaspeh/JDesigner/blob/master/jdesigner/algorithms.py and https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm now – ajlogo Jan 10 '22 at 21:25
  • That's tricky. QPainterPath only provides up to cubic Bézier curves (two control points), and what you're providing uses higher level composite curves. It *could* be done, but I'm afraid that you need to do the math on your own: parse the content of each glyph and then use an algorithm (like the linked one) to add your own contents. But be aware that doing that arbitrarily will probably give unwanted results: there's no way to know if glyph elements do actually require typographic handles or not. There's a reason for which font design is so difficult and requires manual handling of each glyph. – musicamante Jan 11 '22 at 04:59

1 Answers1

1

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]
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • This is quite helpful. Set the calculated midpoint as on curve is the key. Thanks. – ajlogo Jan 21 '22 at 03:38
  • @ajlogo You're welcome! Remember that if an answer solves your problem you should mark it as accepted, by clicking the gray check mark on its left. – musicamante Jan 21 '22 at 11:43