pythongtkgtk3pygobjectgdkpixbuf

Change the colour of pixels in a GdkPixbuf (GTK3)


I'm using Gtk.StatusIcon, and want to change the colour of some pixels; I have a working piece of code, this loads a 1x1 pixel PNG file with the colour I want to set, and then copies that to thhe icon Pixbuf.
While this works, it has the obvious drawback of having to create a 1x1 pixel for every colour, so I can't use an arbitrary colour, only the predefined colours I created images for.

How do I set a pixel to an arbitrary RGBA colour?

The code I'm using now (simplified for demonstration purposes):

#!/usr/bin/env python

from gi.repository import Gtk, GLib, GdkPixbuf

def set_icon_cb(widget, data=None):
    set_icon(widget)

def set_icon(icon):
    fill_amount = 20
    img = GdkPixbuf.Pixbuf.new_from_file('./data/icons/battery.png')
    fill = GdkPixbuf.Pixbuf.new_from_file('./data/icons/green.png')

    for row_num, row in enumerate(zip(*(iter(img.get_pixels()),) *img.get_rowstride())):
        # Blank row
        if 255 not in row: continue

        for col_num, pixel in enumerate(zip(*(iter(row),) * 4)):
            r, g, b, a = pixel

            if col_num > 2 and col_num <= fill_amount and a == 0:
                fill.copy_area(0, 0, 1, 1, img, col_num, row_num)

    icon.set_from_pixbuf(img)


icon = Gtk.StatusIcon()
icon.connect('activate', set_icon_cb)
set_icon(icon)

Gtk.main()

I tried creating a new Pixbuf object with GdkPixbuf.PixbufLoader, but this seems to expect a PNG image, not a bytes object, so this isn't very helpful:

fill = GdkPixbuf.PixbufLoader()
fill.write(b'\x88\x88\x88\xff')
fill.close()
fill = fill.get_pixbuf()

# Gives error:
# GLib.Error: gdk-pixbuf-error-quark: Unrecognized image file format (3)

My next try was to use GdkPixbuf.Pixbuf.new_from_data, which looked promossing:

fill = GdkPixbuf.Pixbuf.new_from_data(b'\xff\x00\xff', GdkPixbuf.Colorspace.RGB,
    False, 8, 1, 1, 3)

However, this doesn't work either. It not only sets the pixels to the wrong colour, it also sets it to different colours on multiple invocations of set_icon(); print(fill.get_pixels()) gives me b'\x00\x00\x00'... Am I using this wrong? I tied various different parameters, but all give the same result...

I also found a C example which modified the result of gdk_pixbuf_get_pixels(), since this returns a pointer to the image data. But this is not something you can do in Python (AFAIK).

A little background of what I'm trying to accomplish:
I have a little tray application to show my laptop's battery status; it fills up the battery icon to give an indication of how much battery power is left. Below a certain percentage the colour changes from green to red (this works with the above example), but I would like to have more flexibility in this; eg. allowing other people to set their own shade of green, or use purple, or whatever.


Solution

  • It turned out to be fairly simple, but not at all obvious from reading the docs:

    # red, green, blue, alpha
    color = 0xeeff2dff
    
    # Create blank 1x1 image
    fill = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 1, 1)
    
    # Fill entire image with color
    fill.fill(color)
    

    My original solution, left here for archival purposes; this is more complicated & doesn't work with transparency, but it might be better suited for more advanced operations

    After more mucking about I ended up using GdkPixbuf.PixbufLoader.new_with_type, from:

    list(map(lambda x: x.get_name(), GdkPixbuf.Pixbuf.get_formats()))
    

    I choose the simplest image format, which appears to be the "portable anymap format" or pnm.

    I created a simple 1x1 image with GIMP, which gave me this data:

    >>> open('test.pnm', 'rb').read()
    b'P6\n# CREATOR: GIMP PNM Filter Version 1.1\n1 1\n255\n\xff\x00\xff'
    

    The last 3 bytes (\xff\x00\xff) is the colour I choose.

    Full example I ended up using:

    color = b'\xee\xff\x2d'
    
    px = GdkPixbuf.PixbufLoader.new_with_type('pnm')
    px.write(b'P6\n\n1 1\n255\n' + color)
    px.write(color)
    px.close()
    fill = px.get_pixbuf()
    

    I can then use fill as in the original example with fill.copy_area()

    It's a bit of a workaround, but acceptable...

    PS.
    From the documentation, GdkPixbuf.Pixbuf.new_from_data looks like the best option to do this, but it seems broken... I can't it to work no matter what I do...