Python: How do you implement a Paintable for GIF animations?

Summary

I’m using GTK 4 and would like to display an animated GIF using a Gtk.Picture widget so that the GIF shrinks or expands depending on the container size.

If I understand correctly, one needs to create a custom class that implements the Gdk.Paintable interface, but I don’t understand how to implement the snapshot method.

Example GIF paintable in C

Looking at GTK’s demo programs, which are written in C, I’ve found an example of an animated GIF and the custom paintable used for it (pixbufpaintable.c):

#include <gtk/gtk.h>
#include "pixbufpaintable.h"

struct _PixbufPaintable {
  GObject parent_instance;

  char *resource_path;
  GdkPixbufAnimation *anim;
  GdkPixbufAnimationIter *iter;

  guint timeout;
};

enum {
  PROP_RESOURCE_PATH = 1,
  NUM_PROPERTIES
};

G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
static void
pixbuf_paintable_snapshot (GdkPaintable *paintable,
                           GdkSnapshot  *snapshot,
                           double        width,
                           double        height)
{
  PixbufPaintable *self = PIXBUF_PAINTABLE (paintable);
  GTimeVal val;
  GdkPixbuf *pixbuf;
  GdkTexture *texture;

  g_get_current_time (&val);
  gdk_pixbuf_animation_iter_advance (self->iter, &val);
  pixbuf = gdk_pixbuf_animation_iter_get_pixbuf (self->iter);
  texture = gdk_texture_new_for_pixbuf (pixbuf);

  gdk_paintable_snapshot (GDK_PAINTABLE (texture), snapshot, width, height);

  g_object_unref (texture);
}
G_GNUC_END_IGNORE_DEPRECATIONS;

static int
pixbuf_paintable_get_intrinsic_width (GdkPaintable *paintable)
{
  PixbufPaintable *self = PIXBUF_PAINTABLE (paintable);

  return gdk_pixbuf_animation_get_width (self->anim);
}

static int
pixbuf_paintable_get_intrinsic_height (GdkPaintable *paintable)
{
  PixbufPaintable *self = PIXBUF_PAINTABLE (paintable);

  return gdk_pixbuf_animation_get_height (self->anim);
}

static void
pixbuf_paintable_init_interface (GdkPaintableInterface *iface)
{
  iface->snapshot = pixbuf_paintable_snapshot;
  iface->get_intrinsic_width = pixbuf_paintable_get_intrinsic_width;
  iface->get_intrinsic_height = pixbuf_paintable_get_intrinsic_height;
}

G_DEFINE_TYPE_WITH_CODE(PixbufPaintable, pixbuf_paintable, G_TYPE_OBJECT,
                        G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
                                               pixbuf_paintable_init_interface))

static void
pixbuf_paintable_init (PixbufPaintable *paintable)
{
}

static gboolean
delay_cb (gpointer data)
{
  PixbufPaintable *self = data;
  int delay;

  delay = gdk_pixbuf_animation_iter_get_delay_time (self->iter);
  self->timeout = g_timeout_add (delay, delay_cb, self);

  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));

  return G_SOURCE_REMOVE;
}

static void
pixbuf_paintable_set_resource_path (PixbufPaintable *self,
                                    const char      *resource_path)
{
  int delay;

  g_free (self->resource_path);
  self->resource_path = g_strdup (resource_path);

  g_clear_object (&self->anim);
  self->anim = gdk_pixbuf_animation_new_from_resource (resource_path, NULL);
  g_clear_object (&self->iter);
  self->iter = gdk_pixbuf_animation_get_iter (self->anim, NULL);

  delay = gdk_pixbuf_animation_iter_get_delay_time (self->iter);
  self->timeout = g_timeout_add (delay, delay_cb, self);

  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));

  g_object_notify (G_OBJECT (self), "resource-path");
}

static void
pixbuf_paintable_set_property (GObject      *object,
                               guint         prop_id,
                               const GValue *value,
                               GParamSpec   *pspec)
{
  PixbufPaintable *self = PIXBUF_PAINTABLE (object);

  switch (prop_id)
    {
    case PROP_RESOURCE_PATH:
      pixbuf_paintable_set_resource_path (self, g_value_get_string (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
pixbuf_paintable_get_property (GObject    *object,
                               guint       prop_id,
                               GValue     *value,
                               GParamSpec *pspec)
{
  PixbufPaintable *self = PIXBUF_PAINTABLE (object);

  switch (prop_id)
    {
    case PROP_RESOURCE_PATH:
      g_value_set_string (value, self->resource_path);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
pixbuf_paintable_dispose (GObject *object)
{
  PixbufPaintable *self = PIXBUF_PAINTABLE (object);

  g_clear_pointer (&self->resource_path, g_free);
  g_clear_object (&self->anim);
  g_clear_object (&self->iter);
  if (self->timeout)
    {
      g_source_remove (self->timeout);
      self->timeout = 0;
    }

  G_OBJECT_CLASS (pixbuf_paintable_parent_class)->dispose (object);
}

static void
pixbuf_paintable_class_init (PixbufPaintableClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->dispose = pixbuf_paintable_dispose;
  object_class->get_property = pixbuf_paintable_get_property;
  object_class->set_property = pixbuf_paintable_set_property;

  g_object_class_install_property (object_class, PROP_RESOURCE_PATH,
      g_param_spec_string ("resource-path", "Resource path", "Resource path",
                           NULL, G_PARAM_READWRITE));

}

GdkPaintable *
pixbuf_paintable_new_from_resource (const char *path)
{
  return g_object_new (PIXBUF_TYPE_PAINTABLE,
                       "resource-path", path,
                       NULL);
}
#+end_src

The paintable is then used like this:

#+begin_src c
paintable = pixbuf_paintable_new_from_resource ("/images/floppybuddy.gif");
picture = gtk_picture_new_for_paintable (paintable);
g_object_unref (paintable);

So I’m trying to translate the code above into Python (as follows).

Example GIF paintable in Python (incomplete)

I think I’ve translated most of the C code, but I don’t know how to finish the Paintable’s snapshot method.

So far, I have a mini app structured like this:

gtk4-animated-gif
├── app.py
├── app-window.ui
└── Muybridge_Buffalo_galloping.gif

Python code (save as app.py):

"""Displaying animated GIFs with Gtk.Picture."""

import sys
import gi

gi.require_version("Gdk", "4.0")
gi.require_version("GdkPixbuf", "2.0")
gi.require_version("Gtk", "4.0")

from gi.repository import Gdk, GdkPixbuf, Gio, GObject, Gtk


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_startup(self):
        Gtk.Application.do_startup(self)

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.present()

    def on_quit(self, action, param):
        self.quit()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    header_bar = Gtk.Template.Child("header-bar")
    image = Gtk.Template.Child("image")
    picture = Gtk.Template.Child("picture")

    def __init__(self, app):
        super().__init__()
        # Display animated GIF.
        gif = "Muybridge_Buffalo_galloping.gif"
        # Set Image.
        self.image.set_from_file(gif)
        # Set Picture.
        paintable = GifPaintable(gif)
        self.picture.set_paintable(paintable)


class GifPaintable(GObject.Object, Gdk.Paintable):
    def __init__(self, path):
        super().__init__()
        self.animation = GdkPixbuf.PixbufAnimation.new_from_file(path)
        self.iterator = self.animation.get_iter()
        self.delay = self.iterator.get_delay_time()
        self.timeout = GObject.timeout_add(self.delay, self.on_delay)

        self.invalidate_contents()

    def on_delay(self):
        delay = self.iterator.get_delay_time()
        self.timeout = GObject.timeout_add(delay, self.on_delay)
        self.invalidate_contents()

        # return G_SOURCE_REMOVE (?)

    def get_intrinsic_height(self):
        return self.animation.get_height()

    def get_intrinsic_width(self):
        return self.animation.get_width()

    def invalidate_contents(self):
        self.emit("invalidate-contents")

    def snapshot(self, snapshot, width, height):
        self.iterator.advance(GObject.get_current_time())
        pixbuf = self.iterator.get_pixbuf()
        texture = Gdk.Texture.new_for_pixbuf(pixbuf)

        # TODO: And then what do I do with the texture and snapshot?
        # The GTK images example in C (gtk4-demo) says
        # gdk_paintable_snapshot (GDK_PAINTABLE (texture), snapshot, width, height);
        # But how do I translate that into Python?        


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)

XML UI (save as app-window.ui):

<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.10.3 -->
<interface>
  <!-- interface-name app-window.ui -->
  <requires lib="gtk" version="4.6"/>
  <template class="AppWindow" parent="GtkApplicationWindow">
    <property name="default-height">400</property>
    <property name="default-width">300</property>
    <child type="titlebar">
      <object class="GtkHeaderBar" id="header-bar">
        <child type="title">
          <object class="GtkBox">
            <property name="orientation">vertical</property>
            <property name="valign">center</property>
            <child>
              <object class="GtkLabel">
                <property name="justify">center</property>
                <property name="label">&lt;b&gt;Animated GIF&lt;/b&gt;</property>
                <property name="use-markup">True</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="title-label">
                <property name="css-classes">subtitle</property>
                <property name="ellipsize">middle</property>
                <property name="justify">center</property>
                <property name="label">GdkPaintable &amp; GtkPicture</property>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
    <child>
      <object class="GtkBox">
        <property name="orientation">vertical</property>
        <property name="spacing">6</property>
        <property name="vexpand">True</property>
        <child>
          <object class="GtkImage" id="image">
            <property name="vexpand">True</property>
          </object>
        </child>
        <child>
          <object class="GtkPicture" id="picture">
            <property name="vexpand">True</property>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

And

The app requires the following software in the environment:

gobject-introspection
gtk4
python3
pygobject

Running the application will currently display the window alright, but the animated GIF is not displayed, and I get critical errors that read:

#+begin_example
(app.py:450): Gdk-CRITICAL **: 16:04:18.902: Paintable of type ‘main+GifPaintable’ does not implement GdkPaintable::snapshot
#+end_example

Questions

  1. In GifPaintable.snapshot, what am I supposed to do with the texture object and the snapshot argument?

    In the C code, they use those objects like this:

    gdk_paintable_snapshot (GDK_PAINTABLE (texture), snapshot, width, height);
    

    But I don’t get what gdk_paintable_snapshot is.

Gdk.Texture implements Gdk.Paintable. In C gdk_paintable_snapshot( ... ) is calling the snapshot method from the texture.

So in Python it should translate to something like:

texture.snapshot(snapshot, width, height)
1 Like

(app.py:450): Gdk-CRITICAL **: 16:04:18.902: Paintable of type ‘main+GifPaintable’ does not implement GdkPaintable::snapshot

In pygobject, when you subclass, the virtual functions are prefixed with do_, so your function needs to be named do_snapshot, check: do_snapshot in python code ref.

1 Like

Here’s you paintable updated to work:

from gi.repository import GLib, Gdk, GdkPixbuf, GObject

class GifPaintable(GObject.Object, Gdk.Paintable):
    def __init__(self, path):
        super().__init__()
        self.animation = GdkPixbuf.PixbufAnimation.new_from_file(path)
        self.iterator = self.animation.get_iter()
        self.delay = self.iterator.get_delay_time()
        self.timeout = GLib.timeout_add(self.delay, self.on_delay)

        self.invalidate_contents()

    def on_delay(self):
        delay = self.iterator.get_delay_time()
        self.timeout = GLib.timeout_add(delay, self.on_delay)
        self.invalidate_contents()

        return GLib.SOURCE_REMOVE

    def do_get_intrinsic_height(self):
        return self.animation.get_height()

    def do_get_intrinsic_width(self):
        return self.animation.get_width()

    def invalidate_contents(self):
        self.emit("invalidate-contents")

    def do_snapshot(self, snapshot, width, height):
        timeval = GLib.TimeVal()
        timeval.tv_usec = GLib.get_real_time()
        self.iterator.advance(timeval)
        pixbuf = self.iterator.get_pixbuf()
        texture = Gdk.Texture.new_for_pixbuf(pixbuf)

        texture.snapshot(snapshot, width, height)
1 Like

@rafaelmardojai thanks a lot!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.