Scalable icons?

I want to add icons to buttons etc. in my applications.
This can easily be done using bitmaps (e.g., PNG images).

However these don’t scale. This is a problem when deploying an application to users who have widely varying screen sizes and DPIs.

I’ve tried using a custom ArtProvider 256x256 PNGs and dynamically scaling to 16, 20, 24, etc., pixels in proportion to the DPI (although wxPython seems to report DPI incorrectly on Linux), but the results are poor.

Ideally I’d like to be able to use SVG images and then dynamically draw them as PNGs of the appropriate size (and cache them once drawn). But I can’t see any SVG rendering class in wxPython, only SVGFileDC which outputs SVG.

Surely other wxPython users have encountered (and solved) this problem?

(By comparison, Qt allows the use of SVG images directly for icons and scales them appropriately for you.)

There still isn’t SVG support in wxWidgets, however it looks like better HiDPI cross-platform support may be coming in 4.1.x. I haven’t investigated fully yet but if I understand correctly it involves selecting differently sized images based on the scaling factor of the display.

There is already some support for this on OSX, although if I remember correctly it requires the images to be in the Resources folder of the application bundle, or something like that, so it’s not very easy to use from a non-bundled python application. The discussions about this on wx-dev have been about making things work better and consistently on all the main platforms.

I’ve been playing around with this for the past few weeks.

All the commonly used SVG features are supported, and for those that aren’t, (like text) loading the file into an editor and changing things to simple paths will probably take care of it. The rendering to a wx.GraphicsContext is not quite perfect on all platforms, but it’s very usable and probably close enough to perfect everywhere except the GDI+ backend. The rasterizing to a wx.Bitmap does work very well everywhere.

Here are some examples:

Snap001

Snap002

Snap003

Snap004

Snap005

That looks wonderful. In terms of API will we see widget.SvgImage alongside widget.Bitmap or what?

It’s possible it could eventually grow into something like that, but for now the focus is just adding the ability to get a bitmap from an SVG, and it’s expected it will be used with the existing widget APIs.

Hi there,

I tried using the wx.svg package for wxPython 4.1.0, but I just get this:

AttributeError: ‘module’ object has no attribute ‘svg’

Is it possible to use wx.svg yet?

Did you import wx.svg? It’s a subpackage of the wx package.

>>> import wx
>>> wx.version()
'4.1.0 osx-cocoa (phoenix) wxWidgets 3.1.4'
>>> import wx.svg
>>> print(wx.svg)
<module 'wx.svg' from '/Users/robind/.myPyEnv/Py38-wx41/lib/python3.8/site-packages/wx/svg/__init__.py'>
>>> 

I’m starting to try to use wxPython 4.1 with SVG icons:

import wx
import wx.svg

class ArtProvider(wx.ArtProvider):

    def CreateBitmap(self, artId, _client, size):
        if artId == 'ICON':
            svg = wx.svg.SVGimage.CreateFromFile('images/icon.svg')
            return svg.ConvertToScaledBitmap(size)
        return wx.NullBitmap

Here’s main():

def main():
    app = wx.App()
    wx.ArtProvider.Push(ArtProvider.ArtProvider())
    window = Window.Window(None)
    window.Show()
    app.MainLoop()

And here’s Window:

class Window(wx.Frame):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.Title = wx.App.Get().AppName
        self.SetIcon(wx.ArtProvider.GetIcon('ICON'))

But when I run it it fails on Windows and Linux. Here’s the error on Linux (same error on Windows):

Traceback (most recent call last):
  File "/home/mark/app/myapp/ArtProvider.py", line 16, in CreateBitmap
    return svg.ConvertToScaledBitmap(size)
  File "/home/mark/opt/py38/lib/python3.8/site-packages/wx/svg/__init__.py", line 144, in ConvertToScaledBitmap
    return self.ConvertToBitmap(scale=scale, width=size.width, height=size.height)
  File "/home/mark/opt/py38/lib/python3.8/site-packages/wx/svg/__init__.py", line 115, in ConvertToBitmap
    bmp = wx.Bitmap.FromBufferRGBA(width, height, buf)
RuntimeError: Failed to gain raw access to bitmap data.

Do the SVG samples in the demo give you the same problem?

No, I ran the demo and double-clicked to add a new svg and added my icon.svg and it showed up perfectly. I then changed the filename in my ArtProvider to one of the images:

    def CreateBitmap(self, artId, _client, size):
        if artId == ICON_NAME:
            filename = (
'/home/mark/.wxPython/wxPython-demo-4.1.0/demo/data/accessories-calculator.svg')
            svg = wx.svg.SVGimage.CreateFromFile(filename)
            print(filename, svg)
            #svg = wx.svg.SVGimage.CreateFromFile('images/icon.svg')
            return svg.ConvertToScaledBitmap(size)
        return wx.NullBitmap

And this gives the same error:

/home/mark/.wxPython/wxPython-demo-4.1.0/demo/data/accessories-calculator.svg SVGimageBase: size (48.0, 48.0)

(Songbird:3643): GdkPixbuf-CRITICAL **: 07:48:40.796: gdk_pixbuf_get_has_alpha: assertion 'GDK_IS_PIXBUF (pixbuf)' failed
Traceback (most recent call last):
  File "/home/mark/app/myapp/ArtProvider.py", line 20, in CreateBitmap
    return svg.ConvertToScaledBitmap(size)
  File "/home/mark/opt/py38/lib/python3.8/site-packages/wx/svg/__init__.py", line 144, in ConvertToScaledBitmap
    return self.ConvertToBitmap(scale=scale, width=size.width, height=size.height)
  File "/home/mark/opt/py38/lib/python3.8/site-packages/wx/svg/__init__.py", line 115, in ConvertToBitmap
    bmp = wx.Bitmap.FromBufferRGBA(width, height, buf)
RuntimeError: Failed to gain raw access to bitmap data.

Since you are using the GTK port, is it possible that you are trying to fetch the image from the art provider before the application is fully started up? (Before MainLoop is called.) if so, are you able to defer it until just after the main loop starts?

If that doesn’t seem to be the problem then I’m out of ideas and will need a small runnable sample to be able to investigate further.

Hi Robin,
Attached is Test1.py 28 LOC plus icon.svg and Test2.py 114 LOC including the SVG embedded.
Both fail with the error using Python 3.8 + wxPython 4.1.0 gtk3 wxWidgets 3.1.4.Test1.py (713 Bytes) Test2.py (3.6 KB) icon.zip (1.4 KB) (had to put icon.svg in a .zip).
Error for Test1.py:

Traceback (most recent call last):
  File "Test1.py", line 23, in CreateBitmap
    return svg.ConvertToScaledBitmap(size)
  File "/home/mark/opt/py38/lib/python3.8/site-packages/wx/svg/__init__.py", line 144, in ConvertToScaledBitmap
    return self.ConvertToBitmap(scale=scale, width=size.width, height=size.height)
  File "/home/mark/opt/py38/lib/python3.8/site-packages/wx/svg/__init__.py", line 115, in ConvertToBitmap
    bmp = wx.Bitmap.FromBufferRGBA(width, height, buf)
RuntimeError: Failed to gain raw access to bitmap data.

Error for Test2.py:

Traceback (most recent call last):
  File "Test2.py", line 23, in CreateBitmap
    return svg.ConvertToScaledBitmap(size)
  File "/home/mark/opt/py38/lib/python3.8/site-packages/wx/svg/__init__.py", line 144, in ConvertToScaledBitmap
    return self.ConvertToBitmap(scale=scale, width=size.width, height=size.height)
  File "/home/mark/opt/py38/lib/python3.8/site-packages/wx/svg/__init__.py", line 115, in ConvertToBitmap
    bmp = wx.Bitmap.FromBufferRGBA(width, height, buf)
RuntimeError: Failed to gain raw access to bitmap data.

By default the art provider will pass wx.DefaultSize to the CreateBitmap method. That value is wx.Size(-1, -1) so the bitmap created in ConvertToScaledBitmap is invalid, and that is why the “Failed to gain raw access to bitmap data” exception happens.

So you should pass the desired size to wx.ArtProvider.GetIcon or add some code like

if size == wx.DefaultSize: 
    ...

to CreateBitmap.

Thank you!
I added this at the start of CreateBitmap:

        if size == wx.DefaultSize:
            size = wx.Size(32, 32)

and it works fine now.