Help with high DPI icons

Hi all,

I’m working on redoing the matplotlib wxpython toolbar (https://github.com/matplotlib/matplotlib/issues/16701). As part of that, I’d like to start using their high resolution icons when available. However, I’m struggling with this. Let me just start by defining a high resolution screen as something where the GetContentScaleFactor() function returns >1 (on the screen I’m working with, a mac retina display, it returns 2).

At the moment I’m working on MacOS, but ideally I’d like a solution that works on all platforms.

The toolbar icons are 24x24 in normal size, 48x48 in large size.

Here’s what I have at the moment:

  1. On MacOS, if you have icons with names like back.png and back@2x.png, if you’re on a high resolution screen it automatically grabs the @2x version, and does something either with resizing or virtual pixels to make it come out the right size. It’s basically magic to me, but it works fine.

  2. The matplotlib resources (https://github.com/matplotlib/matplotlib/tree/master/lib/matplotlib/mpl-data/images) don’t fit this naming convention, they instead have names like back.png and back_large.png.

  3. If I rename back_large.png to back@2x.png it works great on MacOS. Image is correct size and sharp (i.e. StaticBitmap size is 24x24)

  4. If I load the large image, it displays at the full size on my monitor, which means scaled up and fuzzy (i.e. StaticBitmap size is 48x48 instead of 24x24)

  5. If I load the large image and scale it by 2, the image is the correct size and fuzzy (i.e. StaticBitmap size is 24x24).

  6. When I load back.png on MacOS with a content scale factor of 2, so it’s really loading back@2x.png, the Bitmap size is 48x48 but the StaticBitmap created from it has a size of 24x24. When I load back_large.png on MacOS with a content scale factor of 2, the Bitmap size is 48x48 and the StaticBitmap created from it has a size of 48x48.

Point 6 makes me feel like there’s some magic scale variable somewhere that I’m missing, or something like that.

Is there a way to handle this in a platform independent (or dependent) way that doesn’t require renaming the matplotlib resources? I know that wxpython 4.10 is introducing some new ways of dealing with scaling, is this something that just has to wait for that release?

My current test platform:
MacOS 10.14
wxpython 4.07post2 (installed via conda from conda-forge)

Thanks!

  • Jesse

Since I’m only allowed to put two links in a post, here’s a relevant post from about 6 months ago:

And here’s another relevant link I’ve looked at:
https://groups.google.com/forum/#!msg/wx-dev/jNLA-zA6zTw/hUIwqpCuBwAJ

I’m also aware that even if I get the images to load and display properly, there seem to be issues with buffered DCs and using high resolution textures. But since I think that is an upstream issue, getting it working as best I can here, and hoping that wxwidgets fixes the underlying issues at some point seems like the best option. A relevant link:

I am unsure how is it relevant and if it will help on mac, but this is how I approach image loading:

    @classmethod
    def loadBitmap(cls, name, location):
        if cls.gen_scale is None:
            cls.gen_scale = wx.GetApp().GetTopWindow().GetContentScaleFactor()
            # cls.gen_scale = 1 if 'wxGTK' in wx.PlatformInfo else wx.GetApp().GetTopWindow().GetContentScaleFactor()
            cls.res_scale = math.ceil(cls.gen_scale)  # We provide no images with non-int scaling factor
        # Find the biggest image we have, according to our scaling factor
        filename = img = None
        current_res_scale = cls.res_scale
        while img is None and current_res_scale > 0:
            filename, img = cls.loadScaledBitmap(name, location, current_res_scale)
            if img is not None:
                break
            current_res_scale -= 1

        if img is None:
            pyfalog.warning("Missing icon file: {0}/{1}".format(location, filename))
            return None

        w, h = img.GetSize()
        extraScale = cls.gen_scale / current_res_scale

        bmp = wx.Bitmap(img.Scale(int(w * extraScale), int(h * extraScale)))
        return bmp

    @classmethod
    def loadScaledBitmap(cls, name, location, scale=1):
        """Attempts to load a scaled bitmap.

        Args:
            name (str): TypeID or basename of the image being requested.
            location (str): Path to a location that may contain the image.
            scale (int): Scale factor of the image variant to load.

        Returns:
            (str, wx.Image): Tuple of the filename that may have been loaded and the image at that location. The
                filename will always be present, but the image may be ``None``.
        """
        filename = "{0}@{1}x.png".format(name, scale)
        img = cls.loadImage(filename, location)
        if img is None and scale == 1:
            filename = "{0}.png".format(name)
            img = cls.loadImage(filename, location)
        return filename, img

I get current scale factor, round to the closest integer up, get corresponding image (in my case, they are stored using convention you mentioned - img@1x.png, img@2x.png etc), load it and do some extra scaling for case of non-integer resizing. Mind you, I just modified this code and haven’t actually tested it yet.

But i haven’t tested it on windows and mac because I can’t find a way to use hires bitmaps on gtk - it seems to upscale them for me. Seems like I will just force scale factor of 1 there.

edit: oh almost forgot, for macOS you have to downsize bitmap - it does not lose any details and gets proper size:

bmp.SetSize((bmp.GetWidth() // scale, bmp.GetHeight() // scale))

I removed this bit from image loader for now since I am testing it on windows and linux mostly.