Links

Categories

Tags


« | Main | »

Introduction to programming an effect

By Jewe | June 19, 2013

An introduction to programming an effect for the IrfanView Filter Sandbox.

Programming Effects

The first thing to do is creating a new text file in our script folder. The easiest way to get there from the plug-in is by choosing “Explore…” from the utility menu.

JewelScript will only compile ANSI or UTF-8 encoded text files, not UTF-16 or higher.

After creating the new file, we need to make sure the Sandbox recognizes it, by renaming the file extension to “jc”. Because we can place other types of files in the script folder as well (for example image files used as resources for scripts), the Sandbox will only load and compile files with this extension.

In our new script file, we need to create a class that implements the Effect interface. Only classes that implement this interface will be listed as an effect in the Sandbox. This allows us to have other classes in the file as well, without the Sandbox “thinking” they are effects.

class MyEffect implements Effect
{
    method MyEffect()
    {
    }

    method DoEffect(Image img)
    {
    }
}

This is the minimum functionality a working effect for the Sandbox must have: A constructor and a DoEffect() method.

The constructor will be called immediately when the Sandbox has loaded and compiled our effect. This happens as soon as the dialog window of the plug-in is opened. The purpose of the constructor is initializing our effect instance. If our effect class has any member variables, they must be initialized here.

Do not allocate large objects in the constructor. Your effect instance will remain alive for as long as the plug-in dialog is open, so it shouldn’t keep large objects in memory if they are only used during DoEffect(). Specifically Rasterizer and Vector4F instances should be allocated locally in the DoEffect() method. If you need to use member variables for large objects for some reason, initialize them with null in the constructor. Allocate them at the top of DoEffect() and set them to null again before DoEffect() exits.

It is not required that we put our class in a new file. We can have any amount of effects in the same file, which has the added bonus that we can create “super effects” — classes that just create instances of other effects and combine them.

While JewelScript normally can import other script files from the local file system, this has been deactivated for the IrfanView Filter Sandbox. So any references to other functions or classes must be placed in the same file.

Notice that DoEffect() gets an Image object passed to it. This is a reference to the image currently loaded in IrfanView. We cannot directly work with this reference, as it exposes almost no methods. Instead, we pass the image on to one of the rasterizers of the sandbox.

Rasterizer Implementations

A rasterizer is an object that allows to manipulate an image. Now is the first decision we must make for our effect. In it’s current state, the Sandbox offers two rasterizer implementations to choose from. Both have their advantages and weak points. It basically comes down to know which one is best for what you’re trying to do.

The RasterizerRGB offers common drawing methods probably known from other applications or libraries. Many of it’s operations are directly or indirectly performed by Windows GDI drawing functions. There are the bread and butter functions that allow you to draw rects, ellipses, lines and text. On top of that, there are also drawing functions that allow advanced image manipulation with blending effects, like alpha, multiply, screen, darken, lighten, and so on.

The RasterizerSSE takes a slightly different approach to image manipulation. It utilizes the power of SSE / SSE2 instructions in most modern processors to allow mass pixel data processing. If the effect can be implemented with this rasterizer, then this is the one to use, since it is superior in both image quality and performance to the RGB rasterizer. However, not all effects can benefit from the vector concept of this rasterizer.

For this introduction, we are just going to fill the user’s selection on the IrfanView image with a color, so the choice isn’t that important. This can be done with both rasterizers. However, since faster is always better, we’re going for the SSE rasterizer.

Earlier versions of the Sandbox required us to import the classes we wanted to use. As of version 1.3, this is no longer necessary. All of the built-in classes are made available to the script effect automatically.
class MyEffect implements Effect
{
    method MyEffect()
    {
    }

    method DoEffect(Image img)
    {
        // create a rasterizer for the selected portion of the image
        RasterizerSSE ras = new RasterizerSSE(img, img.Selection);

        // do something with 'ras'

        // finally, copy 'ras' back to Image
        ras.Finalize();
    }
}

Now we create a SSE rasterizer at the beginning of DoEffect(). Since we only want to manipulate the user’s selection, and our effect doesn’t need access to the selection’s outer bounds (as a blur effect would, for example), it is sufficient to create the rasterizer only for the selection, not the entire image.

For optimal performance and to preserve memory resources, it is important to choose the correct rasterizer size. It doesn’t make sense to copy a 300 MB image, if we’re only going to change a 10×10 pixel selection. The SSE rasterizer requires five times more memory than the original IrfanView image.

Notice the Finalize() call at the end. If the effect doesn’t show up on the image, then we’ve probably forgotten this. This call will actually copy the image data from the rasterizer back to the image. This is because each rasterizer implementation actually copies the source image and converts it into it’s native format. The SSE rasterizer uses normalized floating-point vectors, while the RGB rasterizer uses conventional one-byte-per-channel RGB data.

Between the creation of the rasterizer and it’s finalize call is where we would implement our effect. In this case that’s very simple:

class MyEffect implements Effect
{
    method MyEffect()
    {
    }

    method DoEffect(Image img)
    {
        // create a rasterizer for the selected portion of the image
        RasterizerSSE ras = new RasterizerSSE(img, img.Selection);

        // create a color
        Color c = new Color(40, 60, 80);

        // draw a box on 'ras' entire rectangle
        ras.DrawFilledBox(ras.Rectangle, c, 1.0);

        // finally, copy 'ras' back to Image
        ras.Finalize();
    }
}

Now we’re ready to test our new effect. We don’t need to close and re-open the plug-in between modifications to scripts; that’s the benefit of working in a script language. Once we’re ready, we just select “Recompile” from the utility menu or hit F5, and we can test our changes.

IrfanView Sandbox 4

 

Not very exciting, right? It would be more useful if the user could at least select the color to use. So we need to add some parameters. And while we’re at it, the DrawFilledBox() method has an ‘opacity’ parameter that we could also make available to the user.

class MyEffect implements Effect
{
    Slider Red, Green, Blue, Opacity;

    method MyEffect()
    {
        Red     = new Slider("Color Red",   "", 0, 0, 255, 40);
        Green   = new Slider("Color Green", "", 0, 0, 255, 60);
        Blue    = new Slider("Color Blue",  "", 0, 0, 255, 80);
        Opacity = new Slider("Opacity",     "", 2, 0, 1,   1);
    }

    method Parameter[] GetParameters() { return { Red, Green, Blue, Opacity }; }

    method DoEffect(Image img)
    {
        // create a rasterizer for the selected portion of the image
        RasterizerSSE ras = new RasterizerSSE(img, img.Selection);

        // create a color
        Color c = new Color(Red.Value, Green.Value, Blue.Value);

        // draw a box on 'ras' entire rectangle
        ras.DrawFilledBox(ras.Rectangle, c, Opacity.Value);

        // finally, copy 'ras' back to Image
        ras.Finalize();
    }
}

The class looks a bit more involved already, so let me explain what we’ve done. First we have added four Slider variables to our class. In our constructor, we initialize these variables with Slider instances. We specify their names, as they should appear on the user interface, a unit string that’s empty in this case, their precision, minimum, maximum and default value.

We have also added a new method to our class: GetParameters(). The Sandbox will call this to get an array of our parameters. If we return one, then it will build a settings page from these objects.

IrfanView Sandbox 6

 

Furthermore, we have substituted all hard coded values in our DoEffect() method by references to the respective slider value. If we run our effect again, it will look like this:

IrfanView Sandbox 7

 

Looks much more like an effect that would be useful, doesn’t it? I’ll leave it to you to expand this little demo effect further. Of course you don’t have to start out with a box. You may as well load a bitmap and use it as a texture. Or you could implement ‘Mandelbrot’ by using the SetPixel() method of the rasterizer.

Adding effect information

Now that we have achieved something we can be proud of, we would probably want to boast about it. The effect needs some information about it’s name, author and purpose. For this, the EffectInfo class comes in handy. In order to return it to the Sandbox, we just need to implement the GetEffectInfo() method:

class MyEffect implements Effect
{
    Slider Red, Green, Blue, Opacity;

    method MyEffect()
    {
        Red     = new Slider("Color Red",   "", 0, 0, 255, 40);
        Green   = new Slider("Color Green", "", 0, 0, 255, 60);
        Blue    = new Slider("Color Blue",  "", 0, 0, 255, 80);
        Opacity = new Slider("Opacity",     "", 2, 0, 1,   1);
    }

    method Parameter[] GetParameters() { return { Red, Green, Blue, Opacity }; }

    method EffectInfo GetEffectInfo()
    {
        return new EffectInfo("My Effect", "by Jewe", "Fills the selection with a color.");
    }

    method DoEffect(Image img)
    {
        // create a rasterizer for the selected portion of the image
        RasterizerSSE ras = new RasterizerSSE(img, img.Selection);

        // create a color
        Color c = new Color(Red.Value, Green.Value, Blue.Value);

        // draw a box on 'ras' entire rectangle
        ras.DrawFilledBox(ras.Rectangle, c, Opacity.Value);

        // finally, copy 'ras' back to Image
        ras.Finalize();
    }
}

Topics: filter sandbox | Comments Off on Introduction to programming an effect

Comments are closed.