Capturing a window to a a set of PNG bytes

32 views
Skip to first unread message

Bill Janssen

unread,
Dec 5, 2016, 2:10:02 AM12/5/16
to Kivy users support
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

qua non

unread,
Dec 5, 2016, 3:14:05 AM12/5/16
to kivy-...@googlegroups.com
I guess we should add a option where if the argument given to save(arg) is of type BytesIO / do this automatically... pr please :)


--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Bill Janssen

unread,
Dec 5, 2016, 12:22:14 PM12/5/16
to Kivy users support
Well, a file-like object, perhaps, not just BytesIO.

Bill
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.

Bill Janssen

unread,
Dec 5, 2016, 3:06:20 PM12/5/16
to Kivy users support
Found a bug.  ImageLoader has an undocumented (not present in the base class) method "extensions()", which returns a list of file extensions it will load.  This will be called if in the course of rendering the Widget to the Fbo, a Texture is painted on the Fbo canvas.  If our FakeImageLoader is at the front of the "loaders" list, it will be called during our "export_to_png()" call, and, being missing, the call will fail catastrophically.  IMO, kivy.core.image.__init__.ImageLoader should provide a default here.  But we can also fix it externally, like this:

class FakeImageLoader(ImageLoader):

    @staticmethod
    def extensions():
        return ()

   
@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
Reply all
Reply to author
Forward
0 new messages