pythonpycairo

How to prevent an extra line to be drawn between text and shape?


I'm trying PyCairo bindings for Cairo graphic library. I'm facing a problem when mixing text and arcs. In my code I draw a circle, then a text close to it, within a loop which draws 10 circles and their associated text.

enter image description here

When drawing text using ctx.show_text(), the current position is set to the advance point (the location where the next character should be written). The problem is a line is drawn between the current point and the first point of the subsequent circle.

The only possibility I see is to use ctx.move_to() to move the current point to the arc start point before drawing the circle. This is not a big problem in this case because the first point can be computed easily and I already have a function to convert polar coordinates (angle 0 and distance = radius) to Cartesian coordinates needed by move_to(). Still I would prefer no to do it, as other cases may not be as obvious.

I wonder if I'm missing something and whether there is another possibility, like a command which would delete the current point before drawing the circle. In addition, the problem seems to be linked to drawing text only, because if no text is drawn, there is no line connecting the individual small circles, albeit they are drawn using multiple commands too:

enter image description here

Why does adding text creates the unwanted lines? How can I solve this?

import math
import cairo

def polar_to_cart(cx, cy, angle, dist):
    x = cx + math.cos(angle) * dist
    y = cy - math.sin(angle) * dist
    return x,y

cx,cy = 150,150
two_pi = 2 * math.pi
radius = 100

# 300x300 px surface
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 2*cx, 2*cy)
ctx = cairo.Context(surface)

# Draw large circle
ctx.set_source_rgb(0,0,0)
ctx.set_line_width(1)
ctx.arc(cx, cy, radius, 0, two_pi)
ctx.stroke()

# Draw small circles
n = 10
for i in range(n):
    # Center location
    x,y = polar_to_cart(cx, cy, two_pi / n * i, radius)

    # Draw an empty circle at x,y
    ctx.arc(x, y, 10, 0, two_pi)    
    ctx.set_source_rgb(1,0,0)
    ctx.set_line_width(1)
    ctx.stroke()

    # Name location (bbox center)
    name = str(i)
    xbbc,ybbc = polar_to_cart(cx, cy, two_pi / n * i, radius + 25)

    # Equivalent reference point location
    x_bearing, y_bearing, width, height = ctx.text_extents(name)[:4]
    xrp = xbbc - width/2 - x_bearing
    yrp = ybbc - height/2 - y_bearing

    # Draw name
    ctx.set_source_rgb(0,0,1)
    ctx.select_font_face("Tahoma", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
    ctx.set_font_size(16)
    ctx.move_to(xrp,yrp)
    ctx.show_text(name)

surface.write_to_png('_temp.png')

Solution

  • As the documentation for cairo_show_text explains,

    After this call the current point is moved to the origin of where the next glyph would be placed in this same progression. That is, the current point will be at the origin of the final glyph offset by its advance values. This allows for easy display of a single logical string with multiple calls to cairo_show_text().

    This means that after the text is drawn, the current path consists of a single point. The ctx.arc method appends to the current path, which in this case means joining that point to the first point of the circle by a straight line. This is the unwanted red line.

    To fix this, you can replace the call ctx.show_text(name) with

        ctx.text_path(name)
        ctx.fill()
    

    in which ctx.fill() will implicitly clear the current path after performing the fill. (ctx.stroke() would instead draw the outline of the font.)

    Alternatively, you can add

       ctx.new_path()
    

    after calling ctx.show_text(name) to clear the path, but this is a little less elegant.