There's this nifty method on Widget, export_to_png(filename). But I am trying to avoid the filesystem these days, so I wanted to make it work on byte streams instead. I wanted to pass an instance of io.BytesIO as the 'filename' parameter, and have it filled with a PNG image.
Turns out export_to_png works by creating an Fbo, then drawing the widget on the Fbo. From the Fbo you can then extract the pixmap as a Texture. The code in export_to_png simply calls Texture.save() on that Texture instance. But what Texture.save() does is to create an instance of kivy.core.image.Image, using the texture, and then call the save() method on Image. That, in turn, looks in the list 'loaders' in kivy.core.image.ImageLoader for an ImageLoader class which has a "can_save" method which returns True, and calls the "save" method on that class. So here's how you create a BytesIO with the PNG image in it. First, you need an appropriate ImageLoader class:
class FakeImageLoader(ImageLoader):
@staticmethod
def save(filename, width, height, fmt, pixels, flipped):
return filename.write(png_encode(pixels, width, height, bpp=(4 if fmt == 'rgba' else 3), flipped=flipped))
@staticmethod
def can_save():
return True
I'll leave the implementation of "png_encode" as an exercise to the reader, but the simplest thing to do is to use the Python Image Library (PIL), which you can import with "pip install pillow". Though it's pretty easy to write yourself, from the spec at
https://www.w3.org/TR/2003/REC-PNG-20031110/.
Then, you simply do this:
from io import BytesIO
def capture_widget(w):
ImageLoader.loaders.insert(0, FakeImageLoader)
fakefilename = BytesIO()
try:
w.export_to_png(fakefilename)
finally:
ImageLoader.loaders.remove(FakeImageLoader)
png = fakefilename.getvalue()
return png
The reverse is simpler, because the constructor for kivy.core.image.Image can already take a BytesIO instance as an argument, along with an "ext=" argument, e.g. ext="png", which will construct the image from the bytes of the BytesIO.
Bill