Scaling Icons - Why is pyRevit so good at it?

Hello pyRevit Friends :slight_smile:

Using icons for pyRevit buttons is super easy! You just drop a icon file with any size (but max 96*96) in the button folder and you are done. pyRevit does all the scaling and gives you buttons with high quality icons.

image

Now I´m sitting here trying to create icons for my C# plugins, and whatever I do the quality of the icons is not good. Also the transparent background gets black…

image

These examples use the same .png file as source!

So i wanted to find out what pyRevit does to get that sharp and detailed icons.

https://github.com/eirannejad/pyRevit/blob/cd192bd619c20a8defb15ba55e31012ab09bfc85/pyrevitlib/pyrevit/coreutils/ribbon.py

The interesting part is this one:

    def create_bitmap(self, icon_size):
        """Resamples image and creates bitmap for the given size.

        Icons are assumed to be square.

        Args:
            icon_size (int): icon size (width or height)

        Returns:
            (Imaging.BitmapSource): object containing image data at given size
        """
        mlogger.debug('Creating %sx%s bitmap from: %s',
                      icon_size, icon_size, self.icon_file_path)
        adjusted_icon_size = icon_size * 2
        adjusted_dpi = DEFAULT_DPI * 2
        screen_scaling = HOST_APP.proc_screen_scalefactor

        self.filestream.Seek(0, IO.SeekOrigin.Begin)
        base_image = Imaging.BitmapImage()
        base_image.BeginInit()
        base_image.StreamSource = self.filestream
        base_image.DecodePixelHeight = int(adjusted_icon_size * screen_scaling)
        base_image.EndInit()
        self.filestream.Seek(0, IO.SeekOrigin.Begin)

        image_size = base_image.PixelWidth
        image_format = base_image.Format
        image_byte_per_pixel = int(base_image.Format.BitsPerPixel / 8)
        palette = base_image.Palette

        stride = int(image_size * image_byte_per_pixel)
        array_size = stride * image_size
        image_data = System.Array.CreateInstance(System.Byte, array_size)
        base_image.CopyPixels(image_data, stride, 0)

        scaled_size = int(adjusted_icon_size * screen_scaling)
        scaled_dpi = int(adjusted_dpi * screen_scaling)
        bitmap_source = \
            Imaging.BitmapSource.Create(scaled_size, scaled_size,
                                        scaled_dpi, scaled_dpi,
                                        image_format,
                                        palette,
                                        image_data,
                                        stride)
        return bitmap_source

But it does not help me, I can´t find out what is done here differently.

What I have tried is:
Setting button larger as 32x32, it does not help because it will just get cut of.
Setting DPI programmatically, but it does not help because there seems to be a 96 DPI limit, setting a higher number has no effect.

So I can not do anything else as setting the Icon to 32*32 and 96 DPI, which leads to the bad result.

Anyone had similar problems when scaling icons for C# Plugins? Any Ideas what I can try?
Really appreciate any advice on this topic, I´m out of ideas.

This is the my C# code for scaling.

using System;
using System.Drawing; // Add reference to System.Drawing.Common
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Interop;
using System.Windows.Media;

public static class IconImageClass
{
    [DllImport("gdi32.dll")]
    public static extern bool DeleteObject(IntPtr hObject);

    public static Bitmap BitmapImageToBitmap(BitmapImage bitmapImage)
    {
        using (MemoryStream outStream = new MemoryStream())
        {
            BitmapEncoder enc = new BmpBitmapEncoder();
            enc.Frames.Add(BitmapFrame.Create(bitmapImage));
            enc.Save(outStream);
            using (var bitmap = new Bitmap(outStream))
            {
                return new Bitmap(bitmap);
            }
        }
    }

    public static BitmapSource BitmapToBitmapSource(Bitmap bitmap)
    {
        IntPtr hBitmap = bitmap.GetHbitmap();
        try
        {
            return Imaging.CreateBitmapSourceFromHBitmap(
                hBitmap, IntPtr.Zero, Int32Rect.Empty,
                BitmapSizeOptions.FromEmptyOptions());
        }
        finally
        {
            DeleteObject(hBitmap);
        }
    }

    public static Bitmap ResizeImage(Image image, int width, int height)
    {
        var destRect = new Rectangle(0, 0, width, height);
        var destImage = new Bitmap(width, height);

        destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);

        using (var graphics = Graphics.FromImage(destImage))
        {
            graphics.CompositingMode = CompositingMode.SourceCopy;
            graphics.CompositingQuality = CompositingQuality.HighQuality;
            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.SmoothingMode = SmoothingMode.HighQuality;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

            using (var wrapMode = new ImageAttributes())
            {
                wrapMode.SetWrapMode(WrapMode.TileFlipXY);
                graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
            }
        }
        return destImage;
    }

    public static BitmapSource ScaledIcon(BitmapImage largeIcon, int w, int h)
    {
        return BitmapToBitmapSource(ResizeImage(BitmapImageToBitmap(largeIcon), w, h));
    }
}

edit: I also tried to scale the icons with gimp and using them directly for my plugin, that gives me the same bad result. What magic is pyRevit doing here?

1 Like

I´m far away from understanding what exactly this cheat is that allows the displaying of larger images, but some user from the Revit forum created an equivalent C# code and also gave an explanation.