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:
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.
- Most of the effects are parameterized. The
EffectConfigToken
class is the base class for all specific effect parameter classes.
- 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
.
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.
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.
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).
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()
.
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.
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:
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.
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:
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.
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:
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
.
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:
public void Render(EffectConfigToken properties, RenderArgs dstArgs,
RenderArgs srcArgs, PdnRegion roi)
{
foreach (Rectangle rect in roi.GetRegionScansReadOnlyInt())
{
for (int y = rect.Top; y < rect.Bottom; y++)
{
for (int x = rect.Left; x < rect.Right; x++)
{
}
}
}
}
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:
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:
[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.
[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.
[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.
[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.