Python library for direct Bezier manipulation

Simon CozensSimon Cozens Posts: 724
edited September 2018 in Font Technology
I’ve been asked to write a tool which performs various transformations to outlines in an OTF, and am now trying to get my head around the various parts of the Python font manipulation ecosystem.

Previously I have written plugins for editors which all come with their own libraries for manipulating Beziers, but this one will be a command line tool using fonttools.

What I’m looking for is an independent library which takes an outline in some format and has functions to add extrema and inflections, rotate, translate, offset, simplify, delete and keep shape, split curve at a point, harmonize, etc. - all the usual operations you want when manipulating curves.

Does such a library exist already? It seems like there should be but I have not found anything in fonttools, robofog, etc. - maybe I’m missing the obvious or maybe the documentation isn’t great.

If not, I think it would be useful to have and I wonder if there would be some value in one which also converted between different Bezier representations (array, NSBezierPath, Glyphs, etc.). I’m happy to write one since I will need it for this project.

Comments

  • Some of the operations you mentioned are simple and require one some lines of code (transformations, adding extrem points...). Others are way more complicated (simplify curve, merge curves). 

    There is code available for some...
  • Simon CozensSimon Cozens Posts: 724
    edited September 2018
    I think most of these operations are simple; the hard part is dealing with the various different representations of a curve that different tools use. But as you know the point of having a library is so we don’t all have to be cutting and pasting these different operations again and again.

    Basically I am looking for the Python equivalent of https://pomax.github.io/bezierjs/
  • Converting between the different representations is not that difficult. Write a pen that outputs your internal format. There are pens for NSBezierPath and ttx. 
  • Simon CozensSimon Cozens Posts: 724
    edited September 2018
    When you say “pens”, do you mean that fonttools is the library I should be using for manipulating Beziers? (Or robofab? Or either one of those?)
  • Dave CrosslandDave Crossland Posts: 1,389
    edited September 2018
    Pens is a commonly used pattern for manipulation of outlines, used both in fonttools and robofab
  • @Simon Cozens, regretfully there is none. I have been looking for one from quite some time, but with no success.. I even tough reimplementig pomax.bezierjs with their permission, as I saw others have done so in different languages, but didn't had the time... If you find one, or decide to write one and need a helping hand, seek me out - I am very interested, just do not want to do it all by myself (I presume you also :) )!
  • Jens KutilekJens Kutilek Posts: 338
    edited September 2018
    I have a module that models a "SuperCubic" which consists of one or more consecutive cubic segments defined by 4 point tuples. The Super Cubic can calculate its own extremum points and inflection points, rasterize into line segments, split at a given t or point (on or close to the curve).

    More functions could be added. Especially simplification and harmonizing could be interesting. Maybe the "SuperCubic" is a good starting point because it already can work across multiple cubic segments.

    I could upload it to GitHub if there's any interest.
  • Khaled HosnyKhaled Hosny Posts: 289
    edited September 2018
    FontTools pens can do some of these operations, see TransformPen for example. There are some more pens in https://github.com/robofab-developers/fontPens/, and there is also https://github.com/googlei18n/cu2qu/ for converting cubic to quadratic (and can convert multiple masters in compatible way).
  • Khaled HosnyKhaled Hosny Posts: 289
    edited September 2018
    There is also fontTools.misc.bezierTools and fontTools.misc.symfont and you can probably find more stuff in FontTools.

  • I could upload it to GitHub if there's any interest.
    Please. Sounds like the perfect starting point.
  • @Jens Kutilek I would love to take a look at the Superbezier module also! Thank you in advance!
  • I have developed a suite of functionality for manipulating/modifying Bézier curves and glyph contours that I would consider contributing. It’s been all for my own use so far since I use FontForge for my work, but I tried to keep the bulk of it app-agnostic so it can be used in a more general way.
  • This is my module: jkFontGeometry

    Let me know if you have any questions or suggestions. A demo script for Glyphs that extracts the SuperCubics from the current layer is included to get you started.

    Some functions are provided in a c extension for speed. This is optional, the same functions are available in pure Python from jkFontGeometry.geometry.
  • https://github.com/typemytype/booleanOperations implements boolean Operations such as union / intersection etc. (I would consider this as one of the hardest parts of writing an own library; other things like transformations, extrema, subdivision, joining, expanding a stroke are relatively easy).

    Khaled Hosny already mentioned fontTools.misc.bezierTools: https://github.com/typesupply/defcon and other bigger projects also makes use of fontTools.misc.bezierTools, it may be worth to have a look at them as well, because they implement additional methods.



  • Propped path expanding is not so easy (at least not for me). I figured out my own algorithm to remove overlap but my path expansion is not where I like it to be. Especially the inner curve with small radius.
  • +1 to what Georg said. Actually, path expansion and remove overlap both have some interesting “corner cases,” where things get extra complicated.
  • Okay, I admit: path expansion is tricky when the curvature radius of the path is smaller than the pen radius.
  • There is also https://github.com/dhermes/bezierhttps://bezier.readthedocs.io/en/latest/reference/bezier.html .
    However, it also does not contain any solutions for offset or simplify. But in my understanding there are no generic solutions for those problems, therefore you wont find them in a base library. 
  • Simon CozensSimon Cozens Posts: 724
    edited October 2018
    Well, I've started with my version: https://simoncozens.github.io/beziers.py/index.html

    Here's an example:
    
    fig, ax = plt.subplots()
        points = [
          Point(100,50),
          Point(50,150),
          Point(150,250),
          Point(200,220),
          Point(250,80),
          Point(220,50)
        ]
        path = BezierPath.fromPoints(points)
        tl,br = path.bounds()
        centroid = (tl+br)/2
        path.rotate(centroid, math.pi/2)
        path.balance()
        path.plot(ax)
        path.offset(Point(5,5)).plot(ax, color="red")
        path.offset(Point(-5,-5)).plot(ax, color="green")
        plt.show()
    


  • @Simon Cozens that's beautiful!
  • Yeah, that’s excellent, @Simon Cozens! Nice work!

    What has happened in the top right corner of the offset curves? How come the offset curve handles are showing corners (i.e., not colinear)?
  • The way I'm doing offsetting is slightly sneaky. I sample the original curve, offset the sampled points, and then run a curve fitting algorithm. (What can I say? There isn't a good way to do it, and when all you've got is a hammer...)

    Currently I'm doing that process segment-wise, rather than over the whole path, which is why you were seeing a corner there. It's not hard to change it to collect the samples over the whole path and path-fit to that:


    I could probably improve it a bit by adding more samples around areas of high curvature and less around areas of low curvature; that would smooth out parts like the top right and the left-hand side (around (60,130) or so).
  • The corner is not totally wrong. In tight corners, you should see a corner.
  • Am I too stupid to find it or is the source code not yet on github?
    Ich second Georg Seifert: The old outlining algorithm seems to have better results at the points with high curvature. Be aware that the outlines may be self-intersecting, when you just interpolate offset sample points. It may be a good idea to examine the curvature along the original path with respect to the stroking radius and then only consider the parts that have a curvature radius >= stroking radius to avoid the self intersection.



    Or just remove the self-intersection afterwards (that's what I did).

    Ist there a reason, why you make the list of points redundant (the first and the last point are identical)?
     (255.0,20.0), (385.0,20.0), (526.0,79.0), (566.0,135.0)],
    [ (566.0,135.0), ...
    
  • Here’s the code: https://github.com/simoncozens/beziers.py
    and here are the docs:
    https://simoncozens.github.io/beziers.py/index.html

    The points are deliberately redundant if you ask for the curve as a segment representation, because you want four points in each segment to make a cubic Bézier. If you don’t want that, you can ask for the curve in node list representation.
  • c.g.c.g. Posts: 53
    Here’s the code: https://github.com/simoncozens/beziers.py
    and here are the docs:
    https://simoncozens.github.io/beziers.py/index.html

    The points are deliberately redundant if you ask for the curve as a segment representation, because you want four points in each segment to make a cubic Bézier. If you don’t want that, you can ask for the curve in node list representation.
    Sorry if I resume this four years old thread, but seems what I was looking for since months.

    I'm trying to convert TTF fonts to OTF with FontTools, specifically T2CharStringPen. I noticed that the generated OTF fonts have more point than the TTF sources. This is the code:

    def get_cff_charstrings(self) -> dict:
    """
    Get T2 charstrings to convert a font from TTF to CFF

    :return: A dictionary of charstrings.
    """
    charstrings = {}
    glyph_set = self.getGlyphSet()

    for k, v in glyph_set.items():

    # Remove overlaps and fix contours direction
    pathops_path = pathops.Path()
    pathops_pen = pathops_path.getPen(glyphSet=glyph_set)
    glyph_set[k].draw(pathops_pen)
    pathops_path.simplify()

    # Draw the glyph with T2CharStringPen and get the charstring
    t2_pen = T2CharStringPen(v.width, glyphSet=glyph_set)
    pathops_path.draw(t2_pen)
    charstring = t2_pen.getCharString()
    charstrings[k] = charstring

    return charstrings

    Is there a way to simplify the paths using the tidy() method of BezierPath and pass the new path to pathops pen or t2_pen?

Sign In or Register to comment.