Jump to content

How to Write an Effect Plugin (Tutorials/Resources)


BoltBait

Recommended Posts

So, you want to learn how to write a plugin for paint.net--well, you're in the right place!

 

Getting Ready

 

The first thing you'll need to do is install* CodeLab:

 

*How to install plugins

You may need to configure your system for CodeLab.

When installed properly, you'll find CodeLab in the paint.net menu Effects > Advanced > CodeLab.

 

Tutorials

 

Now, here is a list of tutorials to get you started*:

 

You may also want to read ReMake's "First Steps" tutorials where he leads you by the hand in making various plugins:

 

Sharing Your Work

 

Once you have an effect working the way you want, and you'd like to share it with the other users of paint.net, simply follow these steps:

  • Compress the effect DLL file along with the install.bat file created by CodeLab into a ZIP file. You should find these files on your desktop.
  • Create a thread in the Plugins forum by pressing the Start New Topic button.
  • Fully describe your plugin and attach a few screenshots of what it does. Be sure to include the UI of your effect in the screenshots.
  • Attach the ZIP file to the post using the Choose File button followed by the Attach This File button.
  • If you choose to post your effect's CodeLab script (source code), be sure to paste your CodeLab script inside of a code block (Click the code button <>, select C#, and paste your code into the proper spot. Press the Insert into post button.) 

 

Help

 

If you start writing a plugin and you get stuck, post your code in the Plugin Developer's Forum and someone will try and help you figure it out.

 

Just be be sure to paste your CodeLab script inside of a code block (Click the code button <>, select C#, and paste your code into the proper spot. Press the Insert into post button.)

 

Here is a general list of what is and is not possible when writing a plugin: http://forums.getpaint.net/index.php?/topic/14566-p

 

As you can see, plugins are flexible, but they can't do EVERYTHING.  If you need more capabilities, maybe what you need is a macro recorder.  Try this one: TinyTask.  Or, if you're looking for a command line utility to manipulate .PDN files, try https://www.irfanview.com/  There is a plugin that allows it to view Paint.NET files.

 

 

What About FileType Plugins?

 

How to write a filetype plugin

 

 

*Those tutorials listed in Bold have been rewritten for CodeLab v6.0!

**These tutorials are for Paint.NET v5.0+ plugins

 

  • Upvote 3
Link to comment
Share on other sites

  • 2 months later...
38 minutes ago, Oliver1963 said:

Question: Do I need to first learn C# while writing these, or do the tutorials teach how to use C# too?


The tutorials assume a basic knowledge of C#. But, if you know C, C++, Java, JavaScript, or even Pascal you should be able to follow along just fine. 

Link to comment
Share on other sites

  • 11 months later...

Plugins cannot write to any layer except the current one (i.e. they can't create new layers).

 

 

Link to comment
Share on other sites

Not really the place to ask @aijaz

I suggest you look through the plug-in index here. My TGMagnitude effect may do what you are seeking, with background opacity set to zero. Additionally there are 'stray pixel' removers if it needs cleaning up.

 

Red ochre Plugin pack.............. Diabolical Drawings ................Real Paintings

 

PdnForumSig2.jpg

Link to comment
Share on other sites

  • 2 months later...

I got my project into Visual Studio, which I now feel at home.

 

I am a little confused about the Rectangles of Interest that Paint.NET uses.

 

My Nuget package DataJuggler.PixelDatabase expects me to load the entire image:

 

PixelDatabase pixelDatabase = PixelDatabaseLoader.LoadPixelDatabase(...

 

You can pass in a path or a bitmap:

 

// Load PixelDatabase by path

PixelDatabase pixelDatabase = PixelDatabaseLoader.LoadPixelDatabase(path, StatusUpdate);

 

// Load by Bitmap

PixelDatabase pixelDatabase = PixelDatabaseLoader.LoadPixelDatabase(sourceImage, StatusUpdate);

// status update is just a method to get pixels updated in long operations

 

I realize Paint.NET uses layers, and I read the plug-in can only effect the active layer, so is there a way to get the full active layer as a bitmap (or Image)?

 

Once a PixelDatabase is loaded, I call ApplyQuery and my package handles updating the entire image.

 

Is there a way to execute code before the image is separated into rectangles of interest?

 

If yes, I can call my ApplyQuery method before rectangles are split, and then in your Render methods I only have to copy pixels to the destination surface.

 

For example, my Create Gradient feature needs to know the size of the entire image. I am not sure how to do that from rectangles of interest.

 

My project and site are free, and I plan on making the plug-in free also, but I can pay you a little for your time if you could guide me how to get started.

 

I think people would find my plug-in useful, as it extends Paint.NET for some pretty useful features.

 

Here is a recent feature added, grayscale:

 

Update the red car to gray, and leave (most) of the rest of the image alone:

 

Bitmap Query Language BQL - If you know SQL you are 90% there. Biggest difference is each criteria is on its own line to make parsing simple.

 

Update
Set Grayscale Red
Where
RedMaxDiff > 0
Y > 761

 

More features are shown here If you have time to watch a 5-minute video: (the first two minutes has a comic book story and animated intro, you can skip it):

 

World's Greatest Grayscale:

 

Thanks for your time, I have wanted to do this since last year but Paint.NET was still on .NET Framework and my package on .NET5.

 

Now that both are on .NET6 I don't have any excuse not to get this done.

 

Corby / Data Juggler

 

 

 

 

 

 

 

Car.png

Car White Gray.png

Edited by DataJuggler
Link to comment
Share on other sites

18 hours ago, DataJuggler said:

Is there a way to execute code before the image is separated into rectangles of interest?

 

The source surface is not really separated into ROIs.

 

The two methods that commonly deal with processing the surfaces are,

void Render(Surface dst, Surface src, Rectangle rect)

and (less often)

void OnSetRenderInfo(PropertyBasedEffectConfigToken newToken, RenderArgs DstArgs, RenderArgs SrcArgs)

 

OnSetRenderInfo() is called once per rendering pass, before the calls to Render().

The entire source surface is available as SrcArgs.Surface and the entire destination surface is available as DstArgs.Surface.

 

Render() is called many times, with each call to Render() processing a single ROI.

The entire source surface is available as src, and the entire destination surface is available as dst.

 

The ROI is in the rect argument. It isn't part of a surface; it's just the bounds of the ROI, to let the particular call to Render() know what section of the surface is assigned to it. Within Render() you are free to read anywhere in the source surface, but should only write destination pixels that are within the ROI (and you should write all of them).

 

If the algorithm needs to process the whole surface at once, as is sometimes the case, the processing is done in OnSetRenderInfo(). I'm actually not sure if it's kosher to write to the destination surface in OnSetRenderInfo(). I don't think it is. The usual practice is to create an auxiliary surface the first time OnSetRenderInfo() executes, write the desired pixels for the whole image into it in OnSetRenderInfo() during that and all subsequent calls, then copy the results to the destination surface, an ROI at a time, in Render(). If you create an auxillary surface, you should dispose of it in OnDispose(bool disposing).

 

 

Link to comment
Share on other sites

49 minutes ago, MJW said:

I'm actually not sure if it's kosher to write to the destination surface in OnSetRenderInfo(). I don't think it is.

 

Don't do that!

 

50 minutes ago, MJW said:

create an auxiliary surface the first time OnSetRenderInfo() executes, write the desired pixels for the whole image into it in OnSetRenderInfo() during that and all subsequent calls, then copy the results to the destination surface, an ROI at a time, in Render().

 

Yes, do that!

Link to comment
Share on other sites

I've never tried this (which is why I forgot to mention it), but I believe that for cases where the rendering requires access to the entire destination surface at the same time, the auxiliary surface can be eliminated and the rendering performed in Render() by specifying EffectRenderingSchedule.None. As I understand it, this causes the rendering to be done in a single call to Render().

 

According to a comment by @xod, the format for requesting that it be done that way is:

 

public YourNameEffectPlugin ()
             : base (StaticName, StaticIcon, SubmenuName, new EffectOptions () {Flags = EffectFlags.Configurable, RenderingSchedule = EffectRenderingSchedule.None})
         {
         } 

 

I hope someone who knows more about this than I do will correct me or provide more details.

 

Even if this is possible, it may be better to do only as much as necessary in OnSetRenderInfo(), then complete whatever can be done in parallel in Render() if the algorithm lends itself to that.

Link to comment
Share on other sites

12 minutes ago, MJW said:

I hope someone who knows more about this than I do will correct me or provide more details.

 

EffectRenderingSchedule.None eliminates the multiple calls to Render() and delivers all of the ROI rectangles at once.

Quoting the documentation:

 

Quote

/// <summary>
/// There is only one call to the Effect's Render method, and all regions of interest are
/// delivered at that time. This also means rendering is effectively single threaded.
/// </summary>
/// <remarks>
/// This is the same behavior of the now-obsolete EffectFlags.SingleRenderCall.
/// </remarks>

 

You are  still restricted to only writing within the region specified by the ROI rectangles, so any algorithm that writes to the entire image must still use a separate work surface.

PdnSig.png

Plugin Pack | PSFilterPdn | Content Aware Fill | G'MICPaint Shop Pro Filetype | RAW Filetype | WebP Filetype

The small increase in performance you get coding in C++ over C# is hardly enough to offset the headache of coding in the C++ language. ~BoltBait

 

Link to comment
Share on other sites

54 minutes ago, null54 said:

You are  still restricted to only writing within the region specified by the ROI rectangles, so any algorithm that writes to the entire image must still use a separate work surface.

 

But if you get all the ROIs at once, doesn't that imply you can write into all of them? Otherwise, of what use is EffectRenderingSchedule.None? It used to be called SingleRenderCall, or something like that, which also suggests there's a singe call for all the ROIs.

 

EDIT: Oh, I see what the point is. If there's a selection, there will only be ROIs that cover the selected region, so writing into the unselected destination would (or could) still be disallowed. I seem to recall some knowledgeable person saying that now it's actually okay to write outside the selection, but I could be misremembering. In any case, the safe choice is to use an auxiliary surface. If that's so, though, I still don't quite see the usefulness of EffectRenderingSchedule.None. Wouldn't it be better to do the non-parallel computations in OnSetRenderInfo(), and at least get a little parallelism in the copy from the auxiliary buffer to the destination?

 

EDIT 2: I did remember correctly. In response to a comment saying "Writing outside of the selection breaks the undo command," @toe_head2001 said:

 

Quote

That was fixed in paint.net 4.1.6.

Obviously, writing outside of the selection is still a bad practice, and just wastes compute resources.

 

However, since it's agreed to be bad practice, it should be avoided.

Link to comment
Share on other sites

  • BoltBait featured this topic
  • 11 months later...

Trying to compile a plugin with CodeLab keeps giving me namespace issues: "(CS0116) A namespace cannot directly contain members such as fields or methods.". Even trying to transfer it to Visual Studio or just trying to compile sample plugins yields no positive results. Any leads as to what I'm doing wrong?

Edited by Arco Frio
Link to comment
Share on other sites

Well, that might clear up at least one of the scripts, since it had a space in between names (don't have the time to test it right now). But the other one was named just the standard "MyScript.cs". If it's of any help, here's what the script looks like on CodeLab:
 

Spoiler
void Render(Surface dst, Surface src, Rectangle rect)
{
    int xTileSize = 0;
    int yTileSize = 10;
    int xSpacing = 0;
    int ySpacing = 2;
    int xProcessAmount = 1;
    int yProcessAmount = 1;
    Rectangle fromRect = Rectangle.FromLTRB(0, yTileSize, rect.Right, rect.Bottom);
    Rectangle toRect = Rectangle.FromLTRB(0, yTileSize + ySpacing, rect.Right, rect.Bottom + ySpacing);

    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            Debug.WriteLine("boop"+y);
            xProcessAmount++;
            CurrentPixel = src[x,y];
            if (toRect.Contains(x, y))
            {
                int offSetX = x - toRect.Left + fromRect.Left;
                int offSetY = y - toRect.Top + fromRect.Top;
                CurrentPixel = src[offSetX, offSetY];
            }
            dst[x,y] = CurrentPixel;
        }
    }
}

 

 

This script is a bit pointless for now since I haven't finished it, but by all means I think it should at least compile without errors. Whenever I try to compile it, CodeLab gives me:
"Build Error
I'm sorry, I was not able to build the DLL.

Please check for build errors in the Error List."

 

Checking the error log only gives me an error on a line that doesn't even exist on my code, and the message only appears for a brief moment right after I try to compile/preview: "Error at line -22: The name 'RandomNumberInstanceSeed' does not exist in the current context (CS0103)".

Sorry for being needy, I appreciate any help!

Link to comment
Share on other sites

42 minutes ago, Arco Frio said:

Error at line -22: The name 'RandomNumberInstanceSeed' does not exist in the current context (CS0103)

 

Make sure you're using the latest version of CodeLab (v6.11). I thought I fixed all that Random Number stuff in the latest build.

  • Upvote 1
Link to comment
Share on other sites

image.png.71ed6725f6b43a4b2a355042c539eb7e.png

Yes, my CodeLab version is the latest, I downloaded it just today and am a total beginner on it. Are there any extra steps I should be careful about when installing? All I did was basically download and run the installer.

 

EDIT: Not sure if this helps, but I've noticed the issue only happens when trying to compile a Classic Effect. Any of the other example scripts compile just fine (besides the Filetype example script, which throws "Error at line -97: The name 'Unsafe' does not exist in the current context (CS0103)").

Edited by Arco Frio
Link to comment
Share on other sites

2 hours ago, Arco Frio said:

the Filetype example script, which throws "Error at line -97: The name 'Unsafe' does not exist in the current context (CS0103)

 

Good to know. Thanks.  I'm not an expert in filetype plugins and didn't even work in that area on the last release.

 

2 hours ago, Arco Frio said:

Are there any extra steps I should be careful about

 

One thing that we need to work on...

 

If you're wanting to paste a script into CodeLab in order to work on it, be sure to File > New and choose that type of script.  This sets up the specific CodeLab tab in order to compile that type of script... THEN paste it in.

 

In other words, you shouldn't paste a classic script into a tab setup for a GPU effect.  That won't work.

 

Link to comment
Share on other sites

6 hours ago, BoltBait said:

I thought I fixed all that Random Number stuff in the latest build.

 

FYI, the error disappears as soon as you add some control to the UI.

  • Upvote 1
Link to comment
Share on other sites

7 hours ago, BoltBait said:

If you're wanting to paste a script into CodeLab in order to work on it, be sure to File > New and choose that type of script.  This sets up the specific CodeLab tab in order to compile that type of script... THEN paste it in.

Well, i you're talking about the script I showed, I kind of made it from scratch by using another one as a reference. The other I tested besides the Classic Script were just the examples provided by CodeLab itself.

 

6 hours ago, ReMake said:

FYI, the error disappears as soon as you add some control to the UI.

I'll try that and see if it lets me compile.

 

EDIT: Yes, that was it! I got to compile it and it worked! Thanks a lot for all the help!

Edited by Arco Frio
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...