ActiveBitmaps or quasi-animation

ActiveBitmap.py

A function based on wx.StaticBitmap, capable of accepting multiple bitmaps that can be cycled through using a Timer
    a quasi-animation if you will.
    It can utilise existing Gif files, either broken down into a list of the component parts, or a valid Gif filename

Based originally on the requirement to be able to flash a bitmap to denote that something relevant or of note had occurred.
or an alternative for a pulse progress dialog to denote something is happening

ActiveBitmap will accept between 1 and many bitmaps, which once activated, will display one after the other,
 using a predefined time lapse between each image.
 Upon reaching the last bitmap or defined end of range, it cycles around to the beginning of the series or start of range
  and starts again.
 This continues until stopped, or a given time period or a given number of repetitions.

A sort of hat tip to ActiveText @ ActiveText rather than StaticText

ActiveBitmap.zip (121.7 KB)

As usual, run the python code from the command line.
The code ActiveBitmap.py has a self contained example and AbDemo.py is a small seperate demonstration. The zip file contains all of the images used for the demonstration.

AbDemo

1 Like

For those of you who are used to loading your images via PyEmbeddedImage for convenience, ActiveBitmaps is happy to accept them like this:

#----------------------------------------------------------------------
# This file was generated by /usr/local/bin/img2py
#
from wx.lib.embeddedimage import PyEmbeddedImage

catalog = {}
index = []

amber = PyEmbeddedImage(
    b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAA'
    b'CXBIWXMAAB7CAAAewgFu0HU+AAAAoklEQVQ4y+2SPQ7CMAyF7UokVq7RtSNSbkCuVHGFHigS'
    b'V4gYURfOAFubn6pmYUBNQy5Qj8/252fLAEfgnjhZaqWMAwJrAEAGdCGJXhk/VgGTpZYoPBE4'
    b'A8+ROmX841drtkXfybt25SkMW63JLbH+s6+uAkp3KeUyAAO6UjcDuCogJNGXAGkR1ypAGT/O'
    b'kbqV0TLjixnfK+MtJHmmS7gfn5/HB6dQNfE6m9XUAAAAAElFTkSuQmCC')
index.append('amber')
catalog['amber'] = amber
getamberData = amber.GetData
getamberImage = amber.GetImage
getamberBitmap = amber.GetBitmap
getamberIcon = amber.GetIcon

#----------------------------------------------------------------------
green = PyEmbeddedImage(
    b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAA'
    b'CXBIWXMAAB7CAAAewgFu0HU+AAAAfElEQVQ4y+3RuxHCMBAE0McV4RJcAQkuy5GpgRlVQ0YL'
    b'ogICxwQMLRCYgJ+EYoYNTztvA/GjGdurqwLQYYcBgYxJMteBZbnDuTDYS06Ph3h6TtyXS0mv'
    b'h/hQGirApgWIChAt5VwBcgswVYDtd2D5qh57XHDFAWvJ0T9vuQFdOhKVIaMM1gAAAABJRU5E'
    b'rkJggg==')
index.append('green')
catalog['green'] = green
getgreenData = green.GetData
getgreenImage = green.GetImage
getgreenBitmap = green.GetBitmap
getgreenIcon = green.GetIcon

#----------------------------------------------------------------------
red = PyEmbeddedImage(
    b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADG3pUWHRSYXcgcHJvZmlsZSB0'
    b'eXBlIGV4aWYAAHja7VdtkhwnDP3PKXIEJCEQx+GzKjfI8f2gmdmZ3XWytpOyy9mmGmghxOv3'
    b'BD3jxl9/TvcHLvYSXNBkMcfocYUcMhd0zF9X3jX5sOvz4G+dJ7u7DzBMglaux1SOf4FdXybc'
    b'1qD6bHd2RthOoFvkE1DWyoxOfwQJO192CidQHlcnZkuPUCtfbTuOG8q5I/l5gl1z8eweDSGB'
    b'pa5YSJiHkPhd2+Uj62Ypy44aWOFHotsS3W74IAEhT693J9A/EvRE8q3nXrN/770in8uxyysu'
    b'4+EInXcHSN8nf1P8sLDcEfHzQK23UG9JnrPbnON6uxIiGI0no7y7sbPmwLGCctnTIkrCrein'
    b'XTKK+eIbJO+++YrSKBOD/ekoUKdCk8ZuGzVADDw4oWVuLNtmkjhzW1pJWIUmJ8nSxaBk4+FE'
    b'YOY7Ftrr5r1eI8PKneDKhGC05f9KcX83+C3FzdkWReTtzhVw8coswFjKrRpeEITm0U03wbdy'
    b'5PcP+YNUhYK6aTa8YPH1ClGVXnJLts4CP0V7bSFyqZ8AoAhrK8CQQAFsIFHCLkrMiQg8GgQq'
    b'QM4SuEIBUuUOkBxEIrvExmttzEm0fVk58jLjbIIQKlEStMlSIFYIivxJwZBDRUWDqkZNak6z'
    b'ligxRI0xprgOuZIkhaQpppQs5VRMLJhatGRm2UrmLDgDNcecsuWcS2FXsFBBrAL/AkvlKjVU'
    b'rbGmajXX0pA+LTRtsaVmLbfSuUvHMdFjT9167mWQGzgpRhg64kjDRh5lItemzDB1xpmmzTzL'
    b'XbWj6pvyDarRUY23Ussv3VWD1aV0C0HrONGlGRTjQFA8LQWQ0Lw080Yh8FJuaeYzY1MoA6Qu'
    b'bVynpRgkDINYJ921e1HuQ7o5tQ/pxv+knFvS/RvKOUj3Vrd3VOvrO9e2YtcuXJx6we7D+LDi'
    b'2Mr6qJUfbT8D/deBZmirw3VEF2ZvYYocQ7bZxH4+og9P/Dpy970IXgP4vld7B5n7UW5uyD4R'
    b'fSL6DRD9Orv/8yvyvwo08XMGf6vdF2U30IeGXioBAAABhGlDQ1BJQ0MgcHJvZmlsZQAAeJx9'
    b'kT1Iw1AUhU9TpVJaHewg4pChOlkQFXHUKhShQqgVWnUweekfNDEkKS6OgmvBwZ/FqoOLs64O'
    b'roIg+APi6OSk6CIl3pcUWsR44fE+zrvn8N59gNCoMs3qGgM03TYzqaSYy6+IoVeEEUAUvQjL'
    b'zDJmJSkN3/q6p16quwTP8u/7s6JqwWJAQCSeYYZpE68TT23aBud94hgryyrxOfGoSRckfuS6'
    b'4vEb55LLAs+MmdnMHHGMWCx1sNLBrGxqxJPEcVXTKV/Ieaxy3uKsVWusdU/+wkhBX17iOq0h'
    b'pLCARUgQoaCGCqqwkaBdJ8VChs6TPv5B1y+RSyFXBYwc89iABtn1g//B79laxYlxLymSBLpf'
    b'HOdjGAjtAs2643wfO07zBAg+A1d627/RAKY/Sa+3tfgR0LcNXFy3NWUPuNwBBp4M2ZRdKUhL'
    b'KBaB9zP6pjzQfwuEV725tc5x+gBkaVbpG+DgEBgpUfaaz7t7Ouf2b09rfj/nNHJvuUy2KAAA'
    b'AAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAewgAAHsIBbtB1PgAAAAd0SU1FB+YKAQ0QDLVp'
    b'4NIAAABqSURBVDjL5ZKxDcAgDARPDJGeggkyMWVquszABigboKzwFGmDZdrEkhvr/6y3DN8s'
    b'LWjDZL4JsqAJLsEhiK7NesyadPJAsgEoHkAzAN1zg7ByszdxNQDVEyEaEXbvHyRBEXTBLTjd'
    b'5h/WABsIUODJI+e1AAAAAElFTkSuQmCC')
index.append('red')
catalog['red'] = red
getredData = red.GetData
getredImage = red.GetImage
getredBitmap = red.GetBitmap
getredIcon = red.GetIcon

import wx
from ActiveBitmap import ActiveBitmap

class Frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "ActiveBitmap Demonstration", size=(320, 200))
        panel = wx.Panel(self)
        box = wx.StaticBox(panel, wx.ID_ANY, "Animation", pos=(10, 10), size=(300, 150))

        bitmap2 = ActiveBitmap(box, -1, pos=(10, 50), bitmap=getamberBitmap(), \
                               bitmap_list = [getgreenBitmap(), getredBitmap(),])
        bitmap2.SetTimerPeriod(500)

        self.Show()
        bitmap2.Activate()

app = wx.App()
frame = Frame(None)
app.MainLoop()

As usual bugs, comments and abuse, on a postcard, please!

Hi Rolf,

Wx has some animation supports e.g. wx.adv.Animation and wx.lib.throbber.
Sorry if I misunderstood your intention, I think using both makes the code much shorter to implement.

The wx.adv.Animation is for playing gif animation:

import wx
from wx.adv import AnimationCtrl

class TestPanel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        ctrl = AnimationCtrl(self)
        ctrl.LoadFile('led1s.gif')
        ctrl.BackgroundColour = self.BackgroundColour
        ctrl.Play()

app = wx.App()
frm = wx.Frame(None)
frm.panel = TestPanel(frm)
frm.Show()
app.MainLoop()

The wx.lib.throbber is for fixed-sized bitmap animation.
I made one example of this in my Gist. Sleep tight tonight. · GitHub

1 Like

wasn’t Google+ trying to exploit gifs ? (I hope that wasn’t a bad omen :flushed:)

Hi Komoto,
perhaps I put too much emphasis on the Gif files.

In answer to your point, it’s probably as short, as it is long e.g.

import wx
from ActiveBitmap import ActiveBitmap

class TestPanel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        ctrl = ActiveBitmap(self, -1, bitmap=wx.NullBitmap, bitmap_list=['led1s.gif',])
        ctrl.SetTimerPeriod(50)
        ctrl.Activate()

app = wx.App()
frm = wx.Frame(None)
frm.panel = TestPanel(frm)
frm.Show()
app.MainLoop()

But there are other advantages, because you can, in effect, create your own animations without a Gif file, simply by providing a list of images.

You can create a flashing animation by providing 2 images that toggle between each other.

Other adavantages are that unlike a standard Gif using AnimationCtrl, you can control the speed, the number of repetitions, or the period time to animate the images.

You can also divide the animation into ranges of images.

One option, not specifically mentioned but available is related to the fact that the supplied bitmaps are all resized to be the same size. This means that you can use ActiveBitmap as a dynamically resizing StaticBitmap i.e. throw any bitmap at it and specify a size=(x, y) parameter and it will resize the image for you.
To achieve this you had to add a superfluous wx.NullBitmap in the bitmap_list but I have made this easier to achieve in version 1.1.0. In this latest version you specify the bitmap with a size parameter and can omit the bitmap_list entirely.

Changelog:

1.1.0   Ensure that illegal values cannot be set in the SetRange function
        Test for missing bitmap_list, allowing easier definition of a dynamic resizing bitmap,
         rather than an animation
        Don't start the period timer is there is only a single bitmap.

ActiveBitmap.py (26.6 KB)

Using ActiveBitmap as a resizing StaticBitmap

import wx
from ActiveBitmap import ActiveBitmap

class Frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "ActiveBitmap Demonstration", size=(320, 450))
        panel = wx.Panel(self)
        box = wx.StaticBox(panel, wx.ID_ANY, "Using Active Bitmap for Dynamic resize", pos=(10, 10), size=(300, 400))
        bitmap1 = ActiveBitmap(box, -1, pos=(10, 10), bitmap=wx.Bitmap('4.png'), size=(16, 16))

        bitmap2 = ActiveBitmap(box, -1, pos=(10, 30), bitmap=wx.Bitmap('4.png'), size=(40, 40))

        bitmap3 = ActiveBitmap(box, -1, pos=(10, 60), bitmap=wx.Bitmap('4.png'), size=(80, 80))

        bitmap4 = ActiveBitmap(box, -1, pos=(10, 130), bitmap=wx.Bitmap('4.png'), size=(160, 80))

        bitmap5 = ActiveBitmap(box, -1, pos=(40, 200), bitmap=wx.Bitmap('4.png'), size=(160, 160))

        wx.StaticText(box, -1, "Image is actually 64 x 64", pos=(100, 10))
        self.Show()

app = wx.App()
frame = Frame(None)
app.MainLoop()

Screenshot at 2023-05-04 10-11-06

Finally, ActiveBitmap is not designed to replace AnimationCtrl, it can be an alternative with extra control or it can create quasi-animations from your own images.
As I said, it was based originally on a requirement to be able to flash a bitmap to denote that something relevant or of note had occurred, or as an alternative for a pulse progress dialog to denote something is happening.
I have a tendency to create software with message areas and/or indicator panels, this code allows me to pulse/change indicators, to keep the user informed about the state of processes/actions with a simple visual tool.

I hope that clears up what my intentions were, for this widget.
I apologise for any confusion.

Regards,
Rolf

p.s. Regards your throbber example: Ah! my eyes, my eyes, … Zzzzzzzzzzzzzzz :sleeping:

1 Like

How about adding support for wx.svg? It produces sharper images when rescaling.

import requests
import wx
import wx.svg

def iconify(icon):
    url = "https://api.iconify.design/{}.svg".format(icon.replace(':', '/'))
    content = requests.get(url).content
    return wx.svg.SVGimage.CreateFromBytes(content)

class Panel(wx.Panel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        ## https://icon-sets.iconify.design/fa6-solid/4/
        img = iconify("fa6-solid:4")

        def icon(w, h):
            x = max(w, h)
            bmp = (img.ConvertToScaledBitmap((x, x))    # 1:1 aspect ratio.
                      .ConvertToImage().Rescale(w, h))  # Resize the image.
            return wx.StaticBitmap(self, -1, bmp)

        self.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
        self.Sizer.AddMany((
            icon(32,32), icon(32,64), icon(256,64),
        ))

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    panel = Panel(frm)
    frm.Show()
    app.MainLoop()

image

:memo: The svg file is provided via “Iconify” download site API. You can also copy and paste the content from the site. Font Awesome Solid • Iconify

:roll_eyes: Does anyone know if there are any plans to support animated svg?

1 Like

@komoto48g
Here is version 1.2.0 of ActiveBitmap ActiveBitmap.zip (377.0 KB)
(The bulk of the zip file’s size is attributable to the example images)

Changelog:

1.2.0   Add the PIL image library as it processes images up to 50 times quicker than wx.Image,
         depending on image size and the number of images to process
        The PIL library also seems to cope with 'optimised' Gif files and animated .Webp files

        Include thread for rescaling the images, as this allows us to start displaying the images,
        as soon as the first one is ready.
        (ActiveBitmap will cycle through available images, adding to the animation as more images become available)

        The PIL library remains optional. If it is not available ActiveBitmap reverts to wx.Image.

        The test for a single image at Activate() checks to ensure that the Scaling thread isn't running,
         which would signify that more images are currently be produced.

        Include File Error bitmaps in case something goes wrong during Gif conversion.

ActiveBitmap

I have included in the zip file your example code, adapted to demonstrate a quasi-animated svg image.
The optional inclusion of PIL does open the door to processing svg files via PIL.
There is a plugin available for svg @ https://github.com/gribbg/pillow_svg
although I haven’t managed to get it to work, yet.
I’m not sure if this is the sort of thing you were thinking of.
AbDemosvg

Thank you for information. I’ll play with “pillow_svg” later!

My image is like this: View Iconify using wx.html2 control · GitHub.
It would be nice if wx.adv.AnimationCtrl supports SVG format in the future.

K

pillow_svg requires pillow >=8.4.0 and < 9.0.0, which is drag as I’m on 9.5.0 and I don’t want to load a older version.
I am unable to find any easily accessable tool to convert an animated svg to a gif or any series of bitmaps or other type of image. It seems the whole animated part of svg depends of css or java.
I have asked the maintainer of pillow_svg if they plan to update it to the latest pillow and if they plan to allow for generating something like PIL’s ImageSequence function.
I’ll let you know if I get an answer.

Regards,
Rolf

Introducing Version 1.2.1, which adds the ability to process an animated PNG file

Changelog:
1.2.1   Add ability to process an animated png (x.apng) file as if it were a gif file.
        imghdr cannot recognise an animated png so this version of ActiveBitmap drops the import of imghdr and
         identifies webp, gif and apng files by the presence of 'loop' in the image's PIL info dictionary.
        The loading of an animated png file relies on the PIL image library, as wx.Image does not differentiate
         between a .apng file and a normal .png file, thus rendering it as a single image.

ActiveBitmap.zip (1.1 MB)

Sadly no word from the developer of https://github.com/gribbg/pillow_svg

Hopefully I’ve not broken anything in implementing the change.
As usual, bugs, comments and abuse, on a postcard, please!

1.2.2 Bug fix
When stopping an ActiveBitmap, if a delayed start or a delayed stop was still progress, the timers
were not stopped.
This could lead to a coredump if the programmer had stopped the ActiveBitmap and then destroyed it.
Function Stop(), now stops all timers.

ActiveBitmap_1.2.2.zip (377.3 KB)