Thanks to suggestions from others I've managed to cobble something together that works. I've committed the changes to the repository, but you can go back a few revisions if you want to see the older code. Here's what I do now:
- I flatten the path using bezierPathByFlatteningPath. This gives me straight line segments.
- I then calculate the start and end points of the perpendicular lines at the start and end of each line segment (these lines are as long as the line size should be at this point).
- I create a bezier path containing a parallelogram consisting of the two perpendicular lines plus lines connecting them. This gives a smooth transition in line widths for each segment.
- Once I have the segments, I draw the ending perpendicular lines of the path, plus the sides of each segment (but not the perpendicular lines between segments) into a new bezier path, which I can then fill to draw the stroke with the desired line width variations.
If you want to use this for printing or under HiDPI, you might have to muck with the "flatness" of the bezier path, but for 1x screen display it looks fine.