pygobject/pycairo

curve_to renders incorrectly

morpho-matters opened this issue · 1 comments

Pycairo seems to be incorrectly rendering certain cubic Bezier splines. The joins are incorrect and the line width changes.

bug

Here is the code to reproduce the effect:

import cairo, ctypes

pts = [(5.1502081, 3.69203566),
    (5.15072707, 3.88650738), (4.98411476, 3.88754198), (4.82523962, 3.88857803),
    (4.82529145, 3.88868164), (1.92531232, 3.88878525), (1.92536413, 3.88888889),
    (1.72264052, 3.88888889), (1.42242387, 3.88888889), (1.01697664, 3.45713401)
    ]

def main():
    width, height = 600, 600
    renderData = (ctypes.c_ubyte * (width*height*4))()
    stride = width*4
    surface = cairo.ImageSurface.create_for_data(renderData, cairo.FORMAT_ARGB32,
        width, height, stride
        )
    context = cairo.Context(surface)
    context.set_line_join(cairo.LINE_JOIN_ROUND)
    context.set_source_rgba(0,0,0,1)
    context.set_operator(cairo.OPERATOR_SOURCE)
    context.paint()

    context.scale(100, 100)

    context.move_to(*pts[0])
    for n in range(1, len(pts), 3):
        x1,y1 = pts[n]
        x2,y2 = pts[n+1]
        x,y = pts[n+2]
        context.curve_to(x1,y1, x2,y2, x,y)
    context.set_line_width(0.1)
    context.set_source_rgba(1,1,1,1)
    context.stroke()
    context.get_target().write_to_png("bug.png")

main()

This is running on Python 3.8.1 on Windows 7 with Pycairo 1.23.0.

I suspect this has something to do with how the middle segment is approximately linear (two of the tangent points are almost exactly coincident with their corresponding positional points) but I don't really know. Any help appreciated!

Tried to minimize this a bit. Dunno if this is much better than the original. I guess cairo-the-C-library has some kind of over/underflow somewhere.

import cairo, ctypes, math

def main():
    width, height = 600, 600
    surface = cairo.ImageSurface(cairo.FORMAT_RGB24, width, height)
    context = cairo.Context(surface)

    context.scale(100, 100)

    # The actual curve that breaks
    context.curve_to(4.82529145, 3.88868164,
       1.92531232, 3.88878525,
       1.92536413, 3.88888889)

    # Just for comparison, a "proper" line with this line width
    if False:
        context.rel_line_to(0, -1)

    context.set_line_width(0.1)
    context.set_source_rgb(1,1,1)
    context.stroke()

    # Show the control points for the curve
    if False:
        for i, (x, y) in enumerate(
                [(4.82529145, 3.88868164),
                 (1.92531232, 3.88878525),
                 (1.92536413, 3.88888889)]):
            color = [0, 0, 0, 0.5]
            color[i] = 1
            context.set_source_rgba(*color)
            context.arc(x, y, 0.05, 0, 2*math.pi)
            context.fill()

    context.get_target().write_to_png("bug.png")

main()