How to Invert Color of XAML PNG Images using C#?
Asked Answered
A

1

10

I'm using Visual Studio, C#, XAML, WPF.

In my program I have XAML buttons with white png icons.

enter image description here

I want to have it so you can switch to a theme with black icons by choosing the theme from a ComboBox.

Instead of creating a new set of black png images, is there a way with XAML and C# I can invert the color of the white icons?

<Button x:Name="btnInfo" HorizontalAlignment="Left" Margin="10,233,0,0" VerticalAlignment="Top" Width="22" Height="22" Cursor="Hand" Click="buttonInfo_Click" Style="{DynamicResource ButtonSmall}">
    <Image Source="Resources/Images/info.png" Width="5" Height="10" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0,0,0"/>
</Button>
Auschwitz answered 14/7, 2017 at 1:54 Comment(2)
These links may help: Using ColorMatrix for Creating Negative Image and WPF - Modifying Image Colors on the Fly (C#)Buiron
This library comes with several predefined shader effects for WPF, including color inversion.Wessex
T
18

Thanks for this question. It gave me a chance to learn something new. :)

Your goal is, once you know what you're doing, very easy to achieve. WPF supports the use of GPU shaders to modify images. They are fast at run-time (since they execute in your video card) and easy to apply. And in the case of the stated goal to invert the colors, very easy to implement as well.

To start with, you'll need the shader code. Shaders are written in a language called High Level Shader Language, or HLSL. Here is an HLSL "program" that will invert the input color:

sampler2D input : register(s0);

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float4 color = tex2D(input, uv);
    float alpha = color.a;

    color = 1 - color;
    color.a = alpha;
    color.rgb *= alpha;

    return color;
}

But, Visual Studio doesn't handle this kind of code directly. You'll need to make sure you have the DirectX SDK installed, which will give you the fxc.exe compiler, used to compile shader code.

I compiled the above with this command line:

fxc /T ps_3_0 /E main /Fo<my shader file>.ps <my shader file>.hlsl

Where, of course, you replace <my shader file> with your actual file name.

(Note: I did this manually, but you can of course create a custom build action in your project to do the same.)

You can then include the .ps file in your project, setting the "Build Action" to "Resource".

That done, you now need to create the ShaderEffect class that will use it. That looks like this:

class InvertEffect : ShaderEffect
{
    private static readonly PixelShader _shader =
        new PixelShader { UriSource = new Uri("pack://application:,,,/<my shader file>.ps") };

    public InvertEffect()
    {
        PixelShader = _shader;
        UpdateShaderValue(InputProperty);
    }

    public Brush Input
    {
        get { return (Brush)GetValue(InputProperty); }
        set { SetValue(InputProperty, value); }
    }

    public static readonly DependencyProperty InputProperty =
        ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(InvertEffect), 0);

}

Key points to the above code:

  • You only need one copy of the shader itself. So I initialize this into a static readonly field. Since the .ps file is included as a resource, I can refer to it using the pack: scheme, as "pack://application:,,,/<my shader file>.ps". Again, you will need to replace <my shader file> with the actual file name, of course.
  • In the constructor, you must set the PixelShader property to the shader object. You must also call UpdateShaderValue() to initialize the shader, for each property used as input to the shader (in this case, there's only the one).
  • The Input property is special: it requires the use of RegisterPixelShaderSamplerProperty() to register the dependency property.
  • If your shader had other parameters, they would be registered normally with DependencyProperty.Register(). But they would require a special PropertyChangedCallback value, obtained by calling ShaderEffect.PixelShaderConstantCallback() with the register index declared in the shader code for that parameter.

That's all there is to it!

You can use the above in XAML simply by setting a UIElement.Effect property to an instance of the InvertEffect class. For example:

<Window x:Class="TestSO45093399PixelShader.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:l="clr-namespace:TestSO45093399PixelShader"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Grid>
    <Rectangle Width="100" Height="100">
      <Rectangle.Fill>
        <LinearGradientBrush>
          <GradientStop Color="Black" Offset="0"/>
          <GradientStop Color="White" Offset="1"/>
        </LinearGradientBrush>
      </Rectangle.Fill>
      <Rectangle.Effect>
        <l:InvertEffect/>
      </Rectangle.Effect>
    </Rectangle>
  </Grid>
</Window>

When you run that, you'll notice that even though the gradient is defined as black in the upper-left transitioning to white in the lower-right, it's displayed the opposite way, with white in the upper-left and black in the lower-right.

Finally, on the off-chance you want to just get this working immediately and don't have access to the fxc.exe compiler, here's a version of the above that has the compiled shader code embedded as Base64. It's tiny, so this is a practical alternative to compiling and including the shader as a resource.

class InvertEffect : ShaderEffect
{
    private const string _kshaderAsBase64 =
@"AAP///7/HwBDVEFCHAAAAE8AAAAAA///AQAAABwAAAAAAQAASAAAADAAAAADAAAAAQACADgAAAAA
AAAAaW5wdXQAq6sEAAwAAQABAAEAAAAAAAAAcHNfM18wAE1pY3Jvc29mdCAoUikgSExTTCBTaGFk
ZXIgQ29tcGlsZXIgMTAuMQCrUQAABQAAD6AAAIA/AAAAAAAAAAAAAAAAHwAAAgUAAIAAAAOQHwAA
AgAAAJAACA+gQgAAAwAAD4AAAOSQAAjkoAIAAAMAAAeAAADkgQAAAKAFAAADAAgHgAAA/4AAAOSA
AQAAAgAICIAAAP+A//8AAA==";

    private static readonly PixelShader _shader;

    static InvertEffect()
    {
        _shader = new PixelShader();
        _shader.SetStreamSource(new MemoryStream(Convert.FromBase64String(_kshaderAsBase64)));
    }

    public InvertEffect()
    {
        PixelShader = _shader;
        UpdateShaderValue(InputProperty);
    }

    public Brush Input
    {
        get { return (Brush)GetValue(InputProperty); }
        set { SetValue(InputProperty, value); }
    }

    public static readonly DependencyProperty InputProperty =
        ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(InvertEffect), 0);

}

Finally, I'll note that the link offered in Bradley's comment does have a whole bunch of these kinds of shader-implemented effects. The author of those implemented the HLSL and the ShaderEffect objects only slightly differently from the way I show here, so if you want to see other examples of effects and different ways to implement them, browsing that code would be a great place to look.

Enjoy!

Trackman answered 14/7, 2017 at 6:49 Comment(1)
Thanks a lot. For future references, using suggested approach in here to calculate the alpha impression on pixel color at first by color.rgb /= color.a and then applying invert by color.rgb = 1 - color.rgb and then add alpha by color.rgb *= color.a made better results for pixels with alpha channel.Siple

© 2022 - 2024 — McMap. All rights reserved.