šŸ› BUG - 8.14 - Rhino.UI.DrawingUtilities.BitmapFromSvg - No Longer Works

Hello,

Prior to 8.14, the Rhino.UI.DrawingUtilities.BitmapFromSvg would ensure that a bitmap derived from SVG code with adjustForDarkMode enabled would modify the SVG fill/stroke color to properly lighten or darken based on the Rhino Theme.

This no longer is working, the adjustForDarkMode value seems to have no effect whatsoever now. Please see the simplified code below to repeat this.

Unfortunately, this bugs out our entire UI as a result since all our icons/buttons rely on SVG to Bitmap and dynamic theme switching support.

Thanks for your help! @CallumSykes @wim @Gijs

System Info

Rhino 8 SR14 2024-11-12 (Rhino 8, 8.14.24317.14001, Git hash:master @ 69ab12ed53cf1e6d17f23ab5fec3c3f7216df72d)
License type: Commercial, build 2024-11-12
License details: Cloud Zoo

Windows 11 (10.0.22631 SR0.0) or greater (Physical RAM: 64GB)
.NET 7.0.20

Computer platform: LAPTOP - Plugged in [98% battery remaining]

Standard graphics configuration.
Primary display: DisplayLink USB Device (DisplayLink) Memory: 0MB, Driver date: 2-9-2024 (M-D-Y).
> External USB display device with 4 adapter port(s)
- Windows Main Display attached to adapter port 0
- Secondary monitor attached to adapter port 1
Primary OpenGL: NVIDIA GeForce RTX 3080 Ti Laptop GPU (NVidia) Memory: 16GB, Driver date: 1-15-2023 (M-D-Y). OpenGL Ver: 4.6.0 NVIDIA 528.24
> Integrated accelerated graphics device with 4 adapter port(s)
- Video pass-through to primary display device

Secondary graphics devices.
Intel(R) Iris(R) Xe Graphics (Intel) Memory: 1GB, Driver date: 10-26-2022 (M-D-Y).
> Integrated graphics device with 4 adapter port(s)
- There are no monitors attached to this device. Laptop lid is probably closed
DisplayLink USB Device (DisplayLink) Memory: 0MB, Driver date: 2-9-2024 (M-D-Y).
> External USB display device with 0 adapter port(s)
- There are no monitors attached to this device. Laptop lid is probably closed

OpenGL Settings
Safe mode: Off
Use accelerated hardware modes: On
GPU Tessellation is: On
Redraw scene when viewports are exposed: On
Graphics level being used: OpenGL 4.6 (primary GPU’s maximum)

Anti-alias mode: 4x
Mip Map Filtering: Linear
Anisotropic Filtering Mode: High

Vendor Name: NVIDIA Corporation
Render version: 4.6
Shading Language: 4.60 NVIDIA
Driver Date: 1-15-2023
Driver Version: 31.0.15.2824
Maximum Texture size: 32768 x 32768
Z-Buffer depth: 24 bits
Maximum Viewport size: 32768 x 32768
Total Video Memory: 16 GB

Rhino plugins that do not ship with Rhino
C:\ProgramData\McNeel\Rhinoceros\7.0\Plug-ins\Datasmith Rhino Exporter (d1fdc795-b334-4933-b680-088119cdc6bb)\DatasmithRhino7.rhp ā€œDatasmith Exporterā€ 5.1.0.0
C:\Program Files\Enscape\Enscape.Rhino.Plugin-net48\Enscape.Rhino8.Plugin.dll ā€œEnscape.Rhino8.Pluginā€ 4.1.1.35
C:\Users\micha\AppData\Roaming\McNeel\Rhinoceros\packages\8.0\doodle\0.1.1-beta+9085\doodle.rhp ā€œdoodleā€ 0.0.0.0
C:\Users\micha\AppData\Roaming\McNeel\Rhinoceros\packages\8.0\Crash\1.4.2-beta\Crash.rhp ā€œCrashā€ 1.4.0.0
C:\Users\micha\AppData\Roaming\McNeel\Rhinoceros\8.0\Plug-ins\D5LiveSync (e0d5e210-02f6-4ee9-a2b0-1675e225d958)\D5Conv.rhp ā€œD5 Live Sync for Rhinoā€

Rhino plugins that ship with Rhino
C:\Program Files\Rhino 8\Plug-ins\Commands.rhp ā€œCommandsā€ 8.14.24317.14001
C:\Program Files\Rhino 8\Plug-ins\rdk.rhp ā€œRenderer Development Kitā€
C:\Program Files\Rhino 8\Plug-ins\RhinoRenderCycles.rhp ā€œRhino Renderā€ 8.14.24317.14001
C:\Program Files\Rhino 8\Plug-ins\rdk_etoui.rhp ā€œRDK_EtoUIā€ 8.14.24317.14001
C:\Program Files\Rhino 8\Plug-ins\NamedSnapshots.rhp ā€œSnapshotsā€
C:\Program Files\Rhino 8\Plug-ins\MeshCommands.rhp ā€œMeshCommandsā€ 8.14.24317.14001
C:\Program Files\Rhino 8\Plug-ins\RhinoCycles.rhp ā€œRhinoCyclesā€ 8.14.24317.14001
C:\Program Files\Rhino 8\Plug-ins\Grasshopper\GrasshopperPlugin.rhp ā€œGrasshopperā€ 8.14.24317.14001
C:\Program Files\Rhino 8\Plug-ins\RhinoCode\RhinoCodePlugin.rhp ā€œRhinoCodePluginā€ 8.14.24317.14001
C:\Program Files\Rhino 8\Plug-ins\Toolbars\Toolbars.rhp ā€œToolbarsā€ 8.14.24317.14001
C:\Program Files\Rhino 8\Plug-ins\3dxrhino.rhp ā€œ3Dconnexion 3D Mouseā€
C:\Program Files\Rhino 8\Plug-ins\Displacement.rhp ā€œDisplacementā€
C:\Program Files\Rhino 8\Plug-ins\SectionTools.rhp ā€œSectionToolsā€

#! python3
import Rhino
import Eto


class UI():
    # Set the color values for all buttons
    h_colors = {
        "bh_color": "#F0F0F0",
        "info": "#D3D3D3"
    }

    misc_icons = {
        # Info Icons
        "info": {
            # Default Icon
            "default": """<?xml version="1.0" encoding="utf-8"?>
                <svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                    x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" overflow="visible" xml:space="preserve">
                <path d="M257,199.29L257,199.29c-27.78,0-50.3,22.52-50.3,50.3v158.3c0,27.78,22.52,50.3,50.3,50.3h0
                    c27.78,0,50.3-22.52,50.3-50.3v-158.3C307.3,221.81,284.78,199.29,257,199.29z"/>
                <path d="M256,55.41L256,55.41c-27.78,0-50.3,22.52-50.3,50.3v0c0,27.78,22.52,50.3,50.3,50.3h0
                    c27.78,0,50.3-22.52,50.3-50.3v0C306.3,77.94,283.78,55.41,256,55.41z"/></svg>""",
            # Hover Icon
            "hover": f"""<?xml version="1.0" encoding="utf-8"?>
                <svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                    x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" overflow="visible" xml:space="preserve">
                <path fill="{h_colors.get("info")}" d="M257,199.29L257,199.29c-27.78,0-50.3,22.52-50.3,50.3v158.3c0,27.78,22.52,50.3,50.3,50.3h0
                    c27.78,0,50.3-22.52,50.3-50.3v-158.3C307.3,221.81,284.78,199.29,257,199.29z"/>
                <path fill="{h_colors.get("info")}" d="M256,55.41L256,55.41c-27.78,0-50.3,22.52-50.3,50.3v0c0,27.78,22.52,50.3,50.3,50.3h0
                    c27.78,0,50.3-22.52,50.3-50.3v0C306.3,77.94,283.78,55.41,256,55.41z"/></svg>"""
        }
    }


# Draw the image
info_icon = UI.misc_icons.get("info")  # Get the svg code for the icon

# Rhino.UI.EtoExtensions.ToEto(Rhino.UI.DrawingUtilities.BitmapFromSvg(info_icon['hover'], 512, 512, False)),  # Hover Icon
bitmap = Rhino.UI.EtoExtensions.ToEto(Rhino.UI.DrawingUtilities.BitmapFromSvg(info_icon['default'], 512, 512, True))  # Default Icon (Dark Mode)
# Rhino.UI.EtoExtensions.ToEto(Rhino.UI.DrawingUtilities.BitmapFromSvg(info_icon['default'], 512, 512)),  # Default Icon (Light Mode)

image_control = Eto.Forms.Button()
image_control.Image = bitmap

form = Eto.Forms.Form()
form.Size = Eto.Drawing.Size(400, 400)

layout = Eto.Forms.DynamicLayout()
layout.AddRow(image_control)

form.Content = layout

form.Show()

EDIT: This seems to also effect this method:

Rhino.UI.ImageResources.CreateEtoIcon(info_icon['default'], 512, 512, False)

Is there something different as of 8.14 that I need to add into my SVG code for this to work? This seems like a bug/regression to me.

Thanks!

Was this utility switching black/white pixels for you correctly for your icons @michaelvollrath, or doing something else?

In the backend there (was) a black/white pixel switcher which I removed. Internally we don’t use it, and I didn’t imagine anyone else was relying on it, so I removed it in favour of the svg syntax system where you can have dark mode attributes inside your svg which is much more customisable, as this offers a lot more flexibility and the two were in conflict.

An example of code that will work for you now.

<svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                    x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" overflow="visible" xml:space="preserve">
                <path fill="{h_colors.get("info")}" fill-dark="probably-white" d="M257,199.29L257,199.29c-27.78,0-50.3,22.52-50.3,50.3v158.3c0,27.78,22.52,50.3,50.3,50.3h0
                    c27.78,0,50.3-22.52,50.3-50.3v-158.3C307.3,221.81,284.78,199.29,257,199.29z"/>
                <path fill="{h_colors.get("info")}" fill-dark="probably-white" d="M256,55.41L256,55.41c-27.78,0-50.3,22.52-50.3,50.3v0c0,27.78,22.52,50.3,50.3,50.3h0
                    c27.78,0,50.3-22.52,50.3-50.3v0C306.3,77.94,283.78,55.41,256,55.41z"/></svg>

1 Like

I think you need to add this:

Rhino.UI.EtoExtensions.UseRhinoStyle(form);
1 Like

Thanks @CallumSykes,

I’m in favor of that change if the svg syntax it supports it so thats great and thanks for sharing the updated code.

When i copy what you provided and replace my previous code (changing ā€œprobably-whiteā€) for a ā€œ#FFFFFFā€ hex value it does not do anything differently.

I don’t see dark-fill as a svg syntax when I search online.

Do you meant that Rhino as of 8.14 will check the svg for ā€œdark-fillā€ variable and somehow update the svg accordingly?

I tried using the code you provided within the code I opened this post with but it does not work for me even when replacing ā€œprobably-whiteā€ with a hex value.

Thanks for your help!

@Gijs Thank you I am using custom drawable graphics so I don’t have the form title bars exposed. I have a toggle button that switches the Rhino Theme through RhinoCommon and then my Eto UI updates the UI colors accordingly and refreshes the controls.

Maybe I don’t understand the issue very well, but if you are updating the UI colors via a button on the form, could you then not simply force the button colors as well in the same button press?

1 Like

You’re right, I can update the fill color when the theme is switched and that would suffice.

Prior I just passed an argument for the pixel switching Callum mentioned to do this automatically I guess but good point in that I can just update my svg fill code with the color i need on theme switch.

Thanks @Gijs, sometimes I need to get my head out of the weeds

Apologies, it is "fill-dark", and "stroke-dark" I have updated my answer above so others don’t get confused.

Thanks @CallumSykes,

The ā€œfill-darkā€ is working now but ā€œstroke-darkā€ does not seem to work.

I’ve tried this way:

        "collapse": {
            # Default Icon
            "default": f"""<?xml version="1.0" encoding="utf-8"?>
                <svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                    x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" overflow="visible" xml:space="preserve" stroke-dark="{h_colors.get("bh_color")}"> 
                <line fill="none" stroke-width="120" stroke-linecap="round" stroke-miterlimit="10" x1="256" y1="340.83" x2="86.34" y2="171.17"/>
                <line fill="none" stroke-width="120" stroke-linecap="round" stroke-miterlimit="10" x1="256" y1="340.83" x2="425.66" y2="171.17"/></svg>""",
            # Hover Icon
            "hover": """<?xml version="1.0" encoding="utf-8"?>
                <svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                    x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" overflow="visible" xml:space="preserve">
                <line fill="none" stroke="#000000" stroke-width="120" stroke-linecap="round" stroke-miterlimit="10" x1="256" y1="340.83" x2="86.34" y2="171.17"/>
                <line fill="none" stroke="#000000" stroke-width="120" stroke-linecap="round" stroke-miterlimit="10" x1="256" y1="340.83" x2="425.66" y2="171.17"/></svg>"""

And this way:

        "collapse": {
            # Default Icon
            "default": f"""<?xml version="1.0" encoding="utf-8"?>
                <svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                    x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" overflow="visible" xml:space="preserve"> 
                <line fill="none" stroke-dark="{h_colors.get("bh_color")}" stroke-width="120" stroke-linecap="round" stroke-miterlimit="10" x1="256" y1="340.83" x2="86.34" y2="171.17"/>
                <line fill="none" stroke-dark="{h_colors.get("bh_color")}" stroke-width="120" stroke-linecap="round" stroke-miterlimit="10" x1="256" y1="340.83" x2="425.66" y2="171.17"/></svg>""",
            # Hover Icon
            "hover": """<?xml version="1.0" encoding="utf-8"?>
                <svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                    x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" overflow="visible" xml:space="preserve">
                <line fill="none" stroke="#000000" stroke-width="120" stroke-linecap="round" stroke-miterlimit="10" x1="256" y1="340.83" x2="86.34" y2="171.17"/>
                <line fill="none" stroke="#000000" stroke-width="120" stroke-linecap="round" stroke-miterlimit="10" x1="256" y1="340.83" x2="425.66" y2="171.17"/></svg>"""
        },

Furthermore the dark-fill only works with this method:

Rhino.UI.ImageResources.CreateEtoIcon(string svgContents, int width, int height, bool adjustForDarkMode)

This method will not adjust the theme colors:

Rhino.UI.EtoExtensions.ToEto(Rhino.UI.DrawingUtilities.BitmapFromSvg(info_icon['default'], 512, 512, True)),  # Default Icon (Dark Mode)

One other thing is switching to the CreateEtoIcon method is significantly slower to create the UI over the BitmapFromSvg method and also does not seem to have the anti-aliasing applied like the BitmapFromSvg method does. Time wise it’s the difference of instant vs 2 seconds when more than 10 icons are present on screen

The svgs produced via CreateEtoIcon are jagged regardless of the size I set for them, is there an additional argument or setting I need to apply somewhere to prevent this?

Thanks for your help!

I will investigate this, do some tests, and get back to you. I didn’t plan to advertise this quite yet :sweat_smile:, but looks like I’ll need to as the pixel flipping was removed. Any fixes for this would be in 8.15 at the earliest, apologies for the regression :slight_smile: .

1 Like

All good @CallumSykes I appreciate you working with me on it and can appreciate that there is likely a lot of svg stuff going on behind the scenes more than just the way I’m trying to use it.

Thanks for looking into it!

1 Like

Hey @michaelvollrath is this fixed now or still an issue? I did a lot of improvements lately so it should be fairly tip top.

1 Like

Awesome! I reverted to my older code but I’ll I’ve this a test again with the fill attributes and test. Thanks for the update!

1 Like

Hi @CallumSykes ,

In my below code I was returning a bitmap from some svg code to then use .DrawSprite() to render it to my screen.

How do I use the CreateEtoIcon with the DrawSprite that expects a bitmap?

The code I commented out actually works perfectly fine except there’s a major problem in that the BitmapFromSvg method of DrawingUtilities seems to change the svg colors even if the AdjustForDarkMode bool is set to False.

Code:

    def GetBitmapFromIcon(icon_name):
        """Return a bitmap from the icon svg code"""
        try:
            if icon_name not in UI.icons:
                print("Icon Not Found")
                return
            
            icon_dict = UI.icons.get(icon_name)  # Get the icon svg code from the dict
            if icon_dict:
                icon = Rhino.UI.ImageResources.CreateEtoIcon(icon_dict, UI.sprite_size, UI.sprite_size, False)
                display_bitmap = Rhino.Rhino.DisplayBitmap(icon)
                # icon_tup = Rhino.UI.DrawingUtilities.BitmapFromSvg(icon_dict, UI.sprite_size, UI.sprite_size, False),  # Default Icon
                # display_bitmap = Rhino.Display.DisplayBitmap(icon_tup[0])

            return display_bitmap

        except Exception as ex:
            Rhino.RhinoApp.WriteLine(f"GetBitmapFromIcon Exception: {ex}")

Thanks for your help!

See here where the svg fill value of ā€œ#2e2e2eā€ yields the circle black instead of dark grey:
image

Expected result visualized in external and online svg editors:

Code to test:

    background_circle = """<circle cx="256" cy="256" r="240" fill=
            "#2E2E2E" if dark_mode else "#C8C8C8"
            stroke="#C8C8C8" if dark_mode else "#2E2E2E" stroke-width="5"/>"""

The Icon class is just a stack of Bitmaps as different scales. You can call the below code.

Icon myIcon = Rhino.UI.ImageResources.CreateEtoIcon(...);
var bitmap = icon.GetFrame(1f).Bitmap;

Let me look into this

1 Like

I’m not quite sure how this relates to the code above or where your dark_mode variable comes from. So I replaced the if/else’s with fill-dark and stroke-dark.

On mac/Windows (9.x) this works perfectly for me I see no issues.

Below is the code I used, does this code work correctly for you?

from Rhino.UI import ImageResources
import Eto.Forms as ef
import Eto.Drawing as ed

from Rhino.Runtime import HostUtils

svg = '''<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 200 200" fill="none">
<circle cx="256" cy="256" r="240" fill="#2E2E2E" fill-dark="#C8C8C8" stroke="#C8C8C8" stroke-dark="#2E2E2E" stroke-width="5"/>
</svg>'''

# on mac I also tried HostUtils.RunningInDarkMode and it worked correctly 
icon = ImageResources.CreateEtoIcon(svg, 200, 200, True) 

image_view = ef.ImageView()
image_view.Image = icon

dialog = ef.Dialog()
dialog.Width = 300
dialog.Height = 300
dialog.Padding = ed.Padding(25)
dialog.Content = image_view

dialog.ShowModal();

Thank you @CallumSykes , that code does work for me and my conditionals are correct.

I will now narrow down exactly where this is occuring. I suspect either in:

Rhino.UI.DrawingUtilities.BitmapFromSvg(icon_dict, UI.sprite_size, UI.sprite_size, False)

or

Something to do with my icon dict/formatting I am doing.

I’ll report back when I know more, thanks for looking into it in the mean time!

EDIT:

    def GetBitmapFromIcon(icon_name):
        """Return a bitmap from the icon svg code to be used for sprite drawing"""
        try:
            if icon_name not in UI.icons:
                print("Icon Not Found")
                return

            svg = UI.UpdateIconBackground(UI.icons.get(icon_name, "bug"), True)#UI.icons.get(icon_name)  # Get the icon svg code from the dict
            
            # if svg:
            #     icon_tup = Rhino.UI.DrawingUtilities.BitmapFromSvg(svg, UI.sprite_size, UI.sprite_size, False),  # Default Icon
            #     display_bitmap = Rhino.Display.DisplayBitmap(icon_tup[0])
            
            if svg:
                icon = Rhino.UI.ImageResources.CreateEtoIcon(svg, 512, 512, True)
                display_bitmap = icon.GetFrame(0).Bitmap
            
            UI.include_background = False

            return display_bitmap

        except Exception as ex:
            Rhino.RhinoApp.WriteLine(f"GetBitmapFromIcon Exception: {ex}")

Okay in this code i’ve updated to use the methods you are showcasing above, however this returns an Eto.Drawing.Bitmap and display conduit drawing sprites requires a Rhino.Display.DisplayBitmap. Is there a helper function to convert from Eto to DisplayBitmap?

DrawForeground Exception Eto.Drawing.Bitmap value cannot be converted to Rhino.Display.DisplayBitmap in method Void DrawSprite(Rhino.Display.DisplayBitmap, Rhino.Geometry.Point3d, Single, Boolean)

I know there’s SystemBitmap but appears to be different? And obviously I’m using an eto svg function for the end use of drawing a rhino display so thats maybe the main error but i dont think there is svg code with the dynamic theme switching for non eto bitmaps is there?

Thanks for clarifying!

The DisplayBitmap constructor will accept a System.Drawing.Bitmap.

display_bitmap = DisplayBitmap(sys_bitmap);

I thought there was a converter in either Eto or Rhino to turn an Eto Bitmap to a System.Drawing.Bitmap, but I can only find it here and I’m not sure if we include this in Rhino and I doubt we would in Rhino for Mac. :thinking:

1 Like

I cannot see any public methods that do this either

1 Like