Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / Windows Forms
Article

Writing effect plug-ins for Paint.NET 2.1 in C#

Rate me:
Please Sign up or sign in to vote.
4.92/5 (17 votes)
10 May 200512 min read 94.6K   1.8K   55   3
This article is an introduction on how to create your own effect plug-ins for Paint.NET 2.1 in C#.

Introduction

Paint.NET 2.1 was released last week. Created to be a free replacement for the good old Paint that ships with every copy of Windows, it is very interesting for end users at large. But it is even more interesting for developers because of two reasons. First, it is open source. So if you like to study a few megabytes of C# code or how some architectural problems can be solved, go and get it. Second, the application provides a simple but appealing interface for creating your own effect plug-ins. And that's what this article is all about (if you're searching for some fancy effect algorithm, go somewhere else as the effect used in the article is quite simple).

Getting started

The first thing you need to do is to get the Paint.NET source code. Besides being the source code, it also serves as its own documentation and as the Paint.NET SDK. The solution consists of several projects. However, the only interesting ones when developing Paint.NET effect plug-ins are the PdnLib library which contains the classes we will use for rendering our effect and the Effects library which contains the base classes for deriving your own effect implementations.

The project basics

To create a new effect plug-in, we start with creating a new C# Class Library and add references to the official release versions of the PdnLib (PdnLib.dll) and the PaintDotNet.Effects library (PaintDotNet.Effects.dll). The root namespace for our project should be PaintDotNet.Effects as we're creating a plug-in that is supposed to fit in seamlessly. This is, of course, not limited to the namespace but more of a general rule: when writing software for Paint.NET, do as the Paint.NET developers do. The actual implementation requires deriving three classes:

  1. Effect is the base class for all Paint.NET effect implementations and it's also the interface Paint.NET will use for un-parameterized effects. It contains the method public virtual void Render(RenderArgs, RenderArgs, Rectangle) which derived un-parameterized effects override.
  2. Most of the effects are parameterized. The EffectConfigToken class is the base class for all specific effect parameter classes.
  3. And finally, as parameterized effects most likely will need a UI, there is a base class for effect dialogs: EffectConfigDialog.

Implementing the infrastructure

Now, we will take a look at the implementation details on the basis of the Noise Effect (as the name implies, it simply adds noise to an image). By the way, when using the sources provided with this article, you will most likely need to update the references to the Paint.NET libraries.

The effect parameters

As I said before, we need to derive a class from EffectConfigToken to be able to pass around our effect parameters. Given that our effect is called Noise Effect and that we want to achieve consistency with the existing sources, our parameter class has to be named NoiseEffectConfigToken.

C#
public class NoiseEffectConfigToken : EffectConfigToken

There is no rule what your constructor has to look like. You can use a simple default constructor or one with parameters. From Paint.NET's point of view, it simply does not matter because (as you will see later) the class (derived from) EffectConfigDialog is responsible for creating an instance of the EffectConfigToken. So, you do not need to necessarily do anything else than having a non-private constructor.

C#
public NoiseEffectConfigToken() : base()
{

}

However, our base class implements the ICloneable interface and also defines a pattern how cloning should be handled. Therefore, we need to create a protected constructor that expects an object of the class' own type and uses it to duplicate all values. We then have to override Clone() and use the protected constructor for the actual cloning. This also means that the constructor should invoke the base constructor but Clone() must not call its base implementation.

C#
protected NoiseEffectConfigToken(NoiseEffectConfigToken copyMe) : base(copyMe)
{

  this.frequency      = copyMe.frequency;
  this.amplitude      = copyMe.amplitude;
  this.brightnessOnly = copyMe.brightnessOnly;

}

public override object Clone()
{
  return new NoiseEffectConfigToken(this);
}

The rest of the implementation details are again really up to you. Most likely, you will define some private fields and corresponding public properties (as the case may be with some plausibility checks).

The UI to set the effect parameters

Now that we've got a container for our parameters, we need a UI to set them. As mentioned before, we will derive the UI dialog from EffectConfigDialog. This is important as it helps to ensure consistency of the whole UI. For example, in Paint.NET 2.0, an effect dialog is by default shown with opacity of 0.9 (except for sessions over terminal services). If I don't use the base class of Paint.NET and the developers decide that opacity of 0.6 is whole lot cooler, my dialog would all of a sudden look "wrong". Because we still try to be consistent with the original code, our UI class is called NoiseEffectConfigDialog.

Again, you have a lot of freedom when it comes to designing your dialog, so I will again focus on the mandatory implementation details. The effect dialog is entirely responsible for creating and maintaining effect parameter objects. Therefore, there are three virtual base methods you must override. And, which might be unexpected, don't call their base implementations (it seems that earlier versions of the base implementations would even generally throw exceptions when called). The first is InitialInitToken() which is responsible for creating a new concrete EffectConfigToken and stores a reference in the protected field theEffectToken (which will implicitly cast the reference to an EffectConfigToken reference).

C#
protected override void InitialInitToken()
{

  theEffectToken = new NoiseEffectConfigToken();

}

Second, we need a method to update the effect token according to the state of the dialog. Therefore, we need to override the method InitTokenFromDialog().

C#
protected override void InitTokenFromDialog()
{

  NoiseEffectConfigToken token = (NoiseEffectConfigToken)theEffectToken;
  token.Frequency      = (double)FrequencyTrackBar.Value / 100.0;
  token.Amplitude      = (double)AmplitudeTrackBar.Value / 100.0;
  token.BrightnessOnly = BrightnessOnlyCheckBox.Checked;

}

And finally, we need to be able to do what we did before the other way round. That is, updating the UI according to the values of a token. That's what InitDialogFromToken() is for. Unlike the other two methods, this one expects a reference to the token to process.

C#
protected override void InitDialogFromToken(EffectConfigToken effectToken)
{

  NoiseEffectConfigToken token = (NoiseEffectConfigToken)effectToken;

  if ((int)(token.Frequency * 100.0) > FrequencyTrackBar.Maximum)
    FrequencyTrackBar.Value = FrequencyTrackBar.Maximum;
  else if ((int)(token.Frequency * 100.0) < FrequencyTrackBar.Minimum)
    FrequencyTrackBar.Value = FrequencyTrackBar.Minimum;
  else
    FrequencyTrackBar.Value = (int)(token.Frequency * 100.0);

  if ((int)(token.Amplitude * 100.0) > AmplitudeTrackBar.Maximum)
    AmplitudeTrackBar.Value = AmplitudeTrackBar.Maximum;
  else if ((int)(token.Amplitude * 100.0) < AmplitudeTrackBar.Minimum)
    AmplitudeTrackBar.Value = AmplitudeTrackBar.Minimum;
  else
    AmplitudeTrackBar.Value = (int)(token.Amplitude * 100.0);

  FrequencyValueLabel.Text = FrequencyTrackBar.Value.ToString("D") + "%";
  AmplitudeValueLabel.Text = AmplitudeTrackBar.Value.ToString("D") + "%";

  BrightnessOnlyCheckBox.Checked = token.BrightnessOnly;

}

We're almost done. What's still missing is that we need to signal the application when values have been changed and about the user's final decision to either apply the changes to the image or cancel the operation. Therefore, whenever a value has been changed by the user, call UpdateToken() to let the application know that it needs to update the preview. Also, call Close() when leaving the dialog and set the appropriate DialogResult. For example:

C#
private void AmplitudeTrackBar_Scroll(object sender, System.EventArgs e)
{

  AmplitudeValueLabel.Text = AmplitudeTrackBar.Value.ToString("D") + "%";
  UpdateToken();

}

private void OkButton_Click(object sender, System.EventArgs e)
{

  DialogResult = DialogResult.OK;
  Close();

}

private void EscButton_Click(object sender, System.EventArgs e)
{

  DialogResult = DialogResult.Cancel;
  Close();

}

Implementing the effect

Now everything is in place to start the implementation of the effect. As I mentioned before, there is a base class for un-parameterized effects. The Noise Effect is parameterized but that will not keep us from deriving from Effect. However, in order to let Paint.NET know that this is a parameterized effect, we need to also implement the IConfigurableEffect interface which adds another overload of the Render() method. It also introduces the method CreateConfigDialog() which allows the application to create an effect dialog.

C#
public class NoiseEffect : Effect, IConfigurableEffect

But how do we construct an Effect object, or in this case, a NoiseEffect object? This time, we have to follow the patterns of the application which means that we use a public default constructor which invokes one of the two base constructors. The first one expects the effect's name, its description, and an icon to be shown in the Effects menu. The second constructor, in addition, requires a shortcut key for the effect. The shortcut key, however, will only be applied to effects which are categorized as an adjustment. In case of a normal effect, it will be ignored (see chapter Effect Attributes for details on effects and adjustments). In conjunction with some resource management, this might look like this:

C#
public NoiseEffect() : base(NoiseEffect.resources.GetString("Text.EffectName"),
  NoiseEffect.resources.GetString("Text.EffectDescription"),
  (Image)NoiseEffect.resources.GetObject("Icons.NoiseEffect.bmp"))
{

}

The only mandatory implementations we need are those that come with the implementation of the interface IConfigurableEffect. Implementing CreateConfigDialog() is quite simple as it does not involve anything but creating a dialog object and returning a reference to it.

C#
public EffectConfigDialog CreateConfigDialog()
{

  return new NoiseEffectConfigDialog();

}

Applying the effect is more interesting but we're going to deal with some strange classes we may never have heard of. So let's first take a look at the signature of the Render() method:

C#
public void Render(EffectConfigToken properties,
                   PaintDotNet.RenderArgs dstArgs,
                   PaintDotNet.RenderArgs srcArgs,
                   PaintDotNet.PdnRegion roi)

The class RenderArgs contains all we need to manipulate images; most important, it provides us with Surface objects which actually allow reading and writing pixels. However, beware not to confuse dstArgs and srcArgs. The object srcArgs (of course, including its Surface) deals with the original image. Therefore, you should never ever perform any write operations on those objects. But you will constantly read from the source Surface as once you made changes to the target Surface, nobody is going to reset those. The target (or destination) Surface is accessible via the dstArgs object. A pixel at a certain point can be easily addressed by using an indexer which expects x and y coordinates. The following code snippet, for example, takes a pixel from the original image, performs an operation, and then assigns the changed pixel to the same position in the destination Surface.

C#
point = srcArgs.Surface[x, y];
VaryBrightness(ref point, token.Amplitude);
dstArgs.Surface[x, y] = point;

But that's not all. The region, represented by the fourth object roi, which the application orders us to manipulate, can have any shape. Therefore, we need to call a method like GetRegionScansReadOnlyInt() to obtain a collection of rectangles that approximate the drawing region. Furthermore, we should process the image line by line beginning at the top. These rules lead to a pattern like this:

C#
public void Render(EffectConfigToken properties, RenderArgs dstArgs,
                   RenderArgs srcArgs, PdnRegion roi)
{

  /* Loop through all the rectangles that approximate the region */
  foreach (Rectangle rect in roi.GetRegionScansReadOnlyInt())
  {
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
      /* Do something to process every line in the current rectangle */
      for (int x = rect.Left; x < rect.Right; x++)
      {
        /* Do something to process every point in the current line */
      }
    }
  }

}

The last interesting fact that should be mentioned is that the Surface class generally uses a 32-bit format with four channels (red, green, blue and alpha) and 8-bits per channel where each pixel is represented by a ColorBgra object. Keep in mind that ColorBgra is actually a struct, so in order to pass an object of that type by reference, you have to use the ref keyword. Furthermore, the struct allows accessing each channel through a public field:

C#
private void VaryBrightness(ref ColorBgra c, double amplitude)
{

  short newOffset = (short)(random.NextDouble() * 127.0 * amplitude);
  if (random.NextDouble() > 0.5)
    newOffset *= -1;

  if (c.R + newOffset < byte.MinValue)
    c.R = byte.MinValue;
  else if (c.R + newOffset > byte.MaxValue)
    c.R = byte.MaxValue;
  else
    c.R = (byte)(c.R + newOffset);

  if (c.G + newOffset < byte.MinValue)
    c.G = byte.MinValue;
  else if (c.G + newOffset > byte.MaxValue)
    c.G = byte.MaxValue;
  else
    c.G = (byte)(c.G + newOffset);

  if (c.B + newOffset < byte.MinValue)
    c.B = byte.MinValue;
  else if (c.B + newOffset > byte.MaxValue)
    c.B = byte.MaxValue;
  else
    c.B = (byte)(c.B + newOffset);

}

Effect Attributes

Now we've got our effect up and running. Is there something else we have to do? Well, in this case everything is fine. However, as every effect is different you might want to apply one of the three attributes that are available in the PaintDotNet.Effects namespace. First, there is the attribute EffectCategoryAttribute which is used to let Paint.NET know if the effect is an effect or an adjustment. The difference between those two is that effects are meant to perform substantial changes on an image and are listed in the Effects menu while adjustments only perform small corrections on the image and are listed in the submenu Adjustments in the menu Layers. Just take a look at the effects and adjustments that are integrated in Paint.NET to get a feeling for how to categorize a certain plug-in. The EffectCategoryAttribute explicitly sets the category of an effect by using the EffectCategory value which is passed to the attribute's constructor. By default, every effect plug-in which does not have an EffectCategoryAttribute is considered to be an effect (and therefore appears in the Effects menu) which is equivalent to applying the attribute as follows:

C#
[EffectCategoryAttribute(EffectCategory.Effect)]

Of course, the enumeration EffectCategory contains two values and the second one, EffectCategory.Adjustment, is used to categorize an effect as an adjustment so that it will appear in the Adjustments submenu in Paint.NET.

C#
[EffectCategoryAttribute(EffectCategory.Adjustment)]

Besides from being able to categorize effects, you can also define your own submenu by applying the EffectSubMenu attribute. Imagine you created ten ultra-cool effects and now want to group them within the Effects menu of Paint.NET to show that they form a toolbox. Now, all you would have to do in order to put all those plug-ins in the submenu 'My Ultra-Cool Toolbox' within the Effects menu would be to apply the EffectSubMenu attribute to every plug-in of your toolbox. This of course can also be done with adjustment plug-ins in order to create submenus within the Adjustments submenu. However, there is one important restriction: because of the way how effects are managed in Paint.NET, the effect name must be unique. This means that you can't have an effect called Foo directly in the Effects menu and a second effect which is also called Foo in the submenu 'My Ultra-Cool Toolbox'. If you try something like this, Paint.NET will call only one of the two effects no matter if you use the command in the Effects menu or the one in the submenu.

C#
[EffectSubMenu("My Ultra-Cool Toolbox")]

Last but not least there is the SingleThreadedEffect attribute. Now, let's talk about multithreading first. In general, Paint.NET is a multithreaded application. That means, for example, that when it needs to render an effect, it will incorporate worker threads to do the actual rendering. This ensures that the UI stays responsive and in case the rendering is done by at least two threads and Paint.NET is running on a multi-core CPU or a multiprocessor system, it also reduces the rendering time significantly. By default, Paint.NET will use as many threads to render an effect as there are logical processors in the system with a minimum number of threads of two.

Processor(s)

physical CPUs

logical CPUs

Threads

Intel Pentium III

1

1

2

Intel Pentium 4 with hyper-threading

1

2

2

Dual Intel Xeon without hyper-threading

2

2

2

Dual Intel Xeon with hyper-threading

2

4

4

However, Paint.NET will use only one thread if the SingleThreadedEffect attribute has been applied regardless of the number of logical processors. If the rendering is done by multiple threads, you have to ensure that the usage of any object in the method Render() is thread-safe. The effect configuration token is usually no problem (as long as you don't change its values, which is not recommended anyway) as the rendering threads get a copy of the token instance used by the UI. Also Paint.NET's own PdnRegion class is thread-safe, accordingly you don't have to worry about those objects. However, GDI+ objects like RenderArgs.Graphics or RenderArgs.Bitmap are not thread-safe so whenever you want to use these objects to render your effect, you have to apply the SingleThreadedEffect attribute. You also may apply the attribute whenever you are not sure if your implementation is actually thread-safe or you simply don't want to ponder about multi-threading. Although doing so will lead to a decreased performance on multiprocessor systems and multi-core CPUs, you'll at least be literally on the safe side.

C#
[SingleThreadedEffect]

Conclusion

Creating effect plug-ins for Paint.NET is not too difficult after all. The parts of the object model you need in order to do this are not very complex (trying this is an excellent idea for the weekend) and it even seems quite robust. Of course, this article does not cover everything there is to know about Paint.NET effect plug-ins but it should be enough to create your first own plug-in.

Acknowledgment

I'd like to thank Rick Brewster and Craig Taylor for their feedback and for proof-reading this article.

Change history

  • 2005-05-08: Added a note that shortcut keys are only applied to adjustments and a chapter about attributes.
  • 2005-01-06: Corrected a major bug in NoiseEffectConfigDialog.InitDialogFromToken(EffectConfigToken). The old implementation used the property EffectConfigDialog.EffectToken instead of the parameter effectToken.
  • 2005-01-03: Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionwhere is the source code of paint.net ???? Pin
kiquenet.com11-Feb-10 10:04
professionalkiquenet.com11-Feb-10 10:04 
GeneralCan't compile the source Pin
edupas18-Jan-05 4:21
edupas18-Jan-05 4:21 
GeneralRe: Can't compile the source Pin
Dennis C. Dietrich18-Jan-05 4:29
Dennis C. Dietrich18-Jan-05 4:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.