More on Pycairo and Transparency: groups and masks

January 22, 2019 ยท 3 minute read

I’ve posted a brief explanation of how I had managed transparency. It worked since I was looking to do a transparent background, but not when I tried something else: OPERATOR_CLEAR is greedy OPERATOR_CLEAR is greedy and will erase the background too. After much tinkering, I found two ways to make it work:

Common Setup

For the sake of an example, I’ll cover the screen with random characters and “erase” rather than paint over. Very useful to mark over a gradient or an image, for example. I use two functions:

def gen_gibberish(n):
    res = []
    for i in range(n):
        res.append(chr(randint(46, 92)))
    return "".join(res)


def write_gib_all(col,font_size=25,i_line = 3, stroke = False, c=c, h =h, w=w):
    # get a font size
    c.select_font_face("/Library/Fonts/Futura.ttc")
    c.set_font_size(font_size)
    f_w = c.text_extents("h").width
    f_h = c.text_extents("h").height
    print("font is {} by {}".format(f_w, f_h))
    # char per row
    n_char = int(round(1.5 * (w/f_w),0))
    n_lin = int(round(1.5 * (h/f_h),0))
    print(" {} lines of {} char".format(n_lin, n_char))

    c.set_source_rgb(*col)
    for j in range(n_lin):
        c.move_to(0,  -i_line + j * (f_h + i_line))
        c.show_text(gen_gibberish(n_char))

    if stroke:
        c.stroke()

I’m then putting characters with write_gib_all(font_col, font_size=15, stroke=True) for example.

If your Mask is a Path: Group/Pop

By path I mean a stack/matrix that can be stroked (stroke()) or filled (fill() or fill_preserve()).

In this case, the solution is to redirect all operation to a temporary (and AFAIK un-referencable) surface with push_group or push_group_with_content to discard alpha values (CONTENT_COLOR).

Calling the code then:

from cairo import Context, SVGSurface, ImageSurface, CONTENT_COLOR, LinearGradient, OPERATOR_CLEAR, SolidPattern
from random import randint

# Image parameters
WIDTH = w = 1000
HEIGHT = h = 1000
bgd_color = (1,1,1)
#Functions

s = SVGSurface('test_group.svg', WIDTH, HEIGHT)
c = Context(s)

#[.. functions defined above]

if __name__ == "__main__":

    bgd = (0,0,0)
    font_col = (1,1,1)
    c.set_source_rgb(*bgd)
    c.paint()

    c.push_group_with_content(CONTENT_COLOR)
    write_gib_all(font_col, font_size=15, stroke=True)

    #blog example

    c.rectangle(300,300,300, 300)
    c.set_source_rgb(1,1,1)
    c.set_operator(OPERATOR_CLEAR)
    c.fill()

    pop_s = c.pop_group_to_source()
    c.paint()

push_group() and pop_group() can be arbitrarily nested, but need matching. The result is:

square carved in letters with Pycairo

If your mask is an image: Mask

I’m currently reading the Beastie Boys Book (which is worth a hardcover purchase IMO), and it brought some found punk memories back. Black Flag is an amazing band, Henri Rollins an outstanding human being, and their logo by Raymond Pettibon truly is an icon of Californian Punk golden days. Don't sue me or beat me up, Black Flag

First, we will instantiate another surface,specifically an ImageSurface


show = True
fil_nam = "blackflag.png"
root_nam = fil_nam.split(".")[0]

m = ImageSurface.create_from_png(fil_nam)

####
# Image parameters
WIDTH = w = m.get_width()
HEIGHT = h = m.get_height()
bgd_color = (1,1,1)
#Functions

s = SVGSurface('{}.svg'.format(root_nam), WIDTH, HEIGHT)
c = Context(s)

We’ll then simply set mask_surface() to the context:


if __name__ == "__main__":

    bgd = (0,0,0)
    font_col = (1,1,1)
    c.set_source_rgb(*bgd)
    c.paint()

    # c.push_group_with_content(CONTENT_COLOR)
    write_gib_all(font_col, font_size=20, stroke=True)
    c.set_source_rgb(*bgd)
    c.mask_surface(m)
    c.fill()

    s.write_to_png("{}_.png".format(root_nam))
    s.finish()

And, voila:

Black Flag in random characters

Of note: painting with alpha or gradients give fun, but sometimes puzzling results