Saving a transparent PNG image from Cairo, in Python
Dave and I will be giving a planetarium talk in February on the analemma and related matters.
Our planetarium, which runs a fiddly and rather limited program called Nightshade, has no way of showing the analemma. Or at least, after trying for nearly a week once, I couldn't find a way. But it can show images, and since I once wrote a Python program to plot the analemma, I figured I could use my program to generate the analemmas I wanted to show and then project them as images onto the planetarium dome.
But naturally, I wanted to project just the analemma and associated labels; I didn't want the blue background to cover up the stars the planetarium shows. So I couldn't just use a simple screenshot; I needed a way to get my GTK app to create a transparent image such as a PNG.
That turns out to be hard. GTK can't do it (either GTK2 or GTK3), and people wanting to do anything with transparency are nudged toward the Cairo library. As a first step, I updated my analemma program to use Cairo and GTK3 via gi.repository. Then I dove into Cairo.
I found one C solution for converting an existing Cairo surface to a PNG, but I didn't have much luck with it. But I did find a Python program that draws to a PNG without bothering to create a GUI. I could use that.
The important part of that program is where it creates a new Cairo "surface", and then creates a "context" for that surface:
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *imagesize) cr = cairo.Context(surface)
A Cairo surface is like a canvas to draw on, and it knows how to save itself to a PNG image. A context is the equivalent of a GC in X11 programming: it knows about the current color, font and so forth. So the trick is to create a new surface, create a context, then draw everything all over again with the new context and surface.
A Cairo widget will already have a function to draw everything (in my case, the analemma and all its labels), with this signature:
def draw(self, widget, ctx):
It already allows passing the context in, so passing in a different context is no problem. I added an argument specifying the background color and transparency, so I could use a blue background in the user interface but a transparent background for the PNG image:
def draw(self, widget, ctx, background=None):
I also had a minor hitch: in draw(), I was saving the context as self.ctx rather than passing it around to every draw routine. That means calling it with the saved image's context would overwrite the one used for the GUI window. So I save it first.
Here's the final image saving code:
def save_image(self, outfile): dst_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.width, self.height) dst_ctx = cairo.Context(dst_surface) # draw() will overwrite self.ctx, so save it first: save_ctx = self.ctx # Draw everything again to the new context, # with a transparent instead of an opaque background: self.draw(None, dst_ctx, (0, 0, 1, 0)) # transparent blue # Restore the GUI context: self.ctx = save_ctx dst_surface.write_to_png("example.png") print("Saved to", outfile)
[ 19:39 Dec 24, 2017 More programming | permalink to this entry | ]