Hello World as a Meta Operation
For reminder, a Meta Operation is a filter which implements a subgraph, i.e. that it uses other existing filters, usually several of them, mixed in a complex enough way that it makes a result interesting enough that you want to automate it (avoiding to redo the same steps of many filters each and every time).
Obviously the filter we are going to develop now is not one of these cases. It’s a Hello World, after all! But it should be enough to get you an idea of how to actually implement all the cool ideas you have with existing filters.
No-Op Meta Operation
The most simple Meta Operation code is basically:
#ifdef GEGL_PROPERTIES
/* none yet */
#else
#define GEGL_OP_META
#define GEGL_OP_NAME gimp_tutorial_meta_op
#define GEGL_OP_C_SOURCE gimp-tutorial-meta-op.c
#include "gegl-op.h"
static void
attach (GeglOperation *operation)
{
GeglNode *gegl = operation->node;
GeglNode *input;
GeglNode *output;
input = gegl_node_get_input_proxy (gegl, "input");
output = gegl_node_get_output_proxy (gegl, "output");
/* construct your GEGL graph here */
gegl_node_link_many (input, output, NULL);
}
static void
gegl_op_class_init (GeglOpClass *klass)
{
GeglOperationClass *operation_class = GEGL_OPERATION_CLASS (klass);
operation_class->attach = attach;
gegl_operation_class_set_keys (operation_class,
"title", "Hello World Meta",
"name", "zemarmot:hello-world-meta",
"categories", "Artistic",
"description", "Hello World as a Meta filter for official GIMP tutorial",
NULL);
}
#endif
Now:
-
compile your file with (for Windows, it will be a
.dll
, and for macOS a.dylib
as output rather than a.so
):gcc -shared gimp-tutorial-meta-op.c `pkg-config --cflags --libs gegl-0.4` -I. -fpic -o gimp-tutorial-meta-op.so
-
Install the generated file in the folder listed previously per your platform standards.
-
Start GIMP.
-
Open the “Search Actions” dialog (either with default shortcut / or from menu
Help > Search and Run a Command
). -
Start typing
Hello World
in the search field (the filter should appear after a few letters at the top of the suggestions) then hit Enter;
You will be greeted by a common filter dialog and you will see on canvas the render of your effect which does… absolutely nothing!
Congratulations! You made your first effect!
Describing the Code
The basic structure
The code of any GEGL operation always has this basic structure:
#ifdef GEGL_PROPERTIES
/* Defining operation properties */
#else
#define GEGL_OP_META
#define GEGL_OP_NAME zemarmot_hello_world_meta
#define GEGL_OP_C_SOURCE gimp-tutorial-meta-op.c
#include "gegl-op.h"
static void
gegl_op_class_init (GeglOpClass *klass)
{
/* Defining various class methods. */
gegl_operation_class_set_keys (operation_class,
"title", "Hello World Meta",
"name", "zemarmot:hello-world-meta",
"categories", "Artistic",
"description", "Hello World as a Meta filter for official GIMP tutorial",
NULL);
}
#endif
The GEGL_PROPERTIES
check separates 2 parts of the code, because the C
file will be self-calling itself several times (through the gegl-op.h
include) and different parts of the code will be read each time.
For this very first operation, we have no properties. So the first part is empty.
Then you must always define:
- a
GEGL_OP_*
among one of the available operation classes. In this first tutorial demo, we create a Meta operation, hence definingGEGL_OP_META
. - a
GEGL_OP_NAME
which is the name of your operation with underscores instead of spaces, hyphens or special character. GEGL_OP_C_SOURCE
to the name of the file itself (here I named itgimp-tutorial-meta-op.c
).
After defining these 3 macros, include gegl-op.h
.
Last but not least, you must create a function called
gegl_op_class_init()
which will be used to implement various class
methods.
The rest of the generic GObject
class structure is generated by
gegl-op.h
.
gegl_op_class_init()
in more details
Depending on which specific type of GEGL Operation you chose, you will
implement different class methods. In the case of a
GeglOperationMeta
, what you want to implement is usually the attach()
method of GeglOperationClass
and sometimes the update()
method of
GeglOperationMetaClass
.
Then we finalize our operation by calling
gegl_operation_class_set_keys()
which is basically where we set a
whole bunch of generic metadata, such as a title (what will show in menu
item name or in the “Action Search”), a longer description (what will
show in tooltips or as sub-title in “Action Search”) and a few more
interesting keys.
In particular, we will choose a “name”. Here we chose “zemarmot:hello-world-meta”. Now we go back to what we were saying for the PDB Procedure name, which is that the name of your GEGL operation too is to be considered as some interface which you want as stable as possible.
So you should choose a descriptive name. Don’t call your operation “my-filter” for instance!
But also do you see the part before the colon: “zemarmot:”. Take this as
your namespace in a standard naming procedure. Create your own namespace
which you believe that nobody will likely use. It may even be a bit long
(it’s not really a user-facing name), and prefix all your operations
with it. Here for instance zemarmot:
is my namespace. Please don’t use
it. 😉
⚠️ Please never prefix your third-party operations with gegl:
or
gimp:
. Consider both these namespaces as reserved by respectively GEGL
and GIMP (eventually we might even forbid third-party operations with
these prefixes; this is something we discuss sometimes). Use any
namespace but these.
Note: GEGL has an aliasing system for operations, so it is in fact possible to fix names afterwards, but choosing wisely your name initially is still better.
attach()
method
A GeglOperationMeta
operation is especially targetted at just running
a subgraph calling other operations. The attach()
method is about
creating the base graph of your operation.
In this very basic case, we take a graph linking the input pad… directly to the output pad.
You guessed it: this filter does nothing! Or to be more accurate, it returned in output what it got in input.
Display Hello World in our Meta Operation
Now let’s spice it all up a bit and actually do something! Since it’s a Hello World demo, we might as well display Hello World!
One of GEGL’s default operation is called gegl:text
. It has all these
arguments:
Properties:
string [gchararray] String to display (utf8) (default: "Hello")
font [gchararray] Font family (utf8) (default: "Sans")
size [gdouble] Font size in pixels. (default: 10.000000)
color [GeglColor] Color for the text (defaults to 'black')
wrap [gint] Sets the width in pixels at which long lines will wrap. Use -1 for no
wrapping. (default: -1)
vertical-wrap [gint] Sets the height in pixels according to which the text is vertically
justified. Use -1 for no vertical justification. (default: -1)
alignment [gint] Alignment for multi-line text (0=Left, 1=Center, 2=Right) (default: 0)
vertical-alignment [gint] Vertical text alignment (0=Top, 1=Middle, 2=Bottom) (default: 0)
width [gint] Rendered width in pixels. (read only) (default: 0)
height [gint] Rendered height in pixels. (read only) (default: 0)
Let’s modify our attach()
function:
static void
attach (GeglOperation *operation)
{
GeglNode *gegl = operation->node;
GeglNode *input;
GeglNode *text;
GeglNode *output;
input = gegl_node_get_input_proxy (gegl, "input");
output = gegl_node_get_output_proxy (gegl, "output");
text = gegl_node_new_child (gegl,
"operation", "gegl:text",
"string", "Hello World!",
"size", 30.0,
NULL);
gegl_node_link_many (input, text, output, NULL);
}
Recompile, restart GIMP (since the operation is in memory, you must
restart GIMP for testing a new version): now that you inserted a
gegl:text
node in the middle of your graph, what this operation does
is replacing the input with the text.
Now you may notice that you see this warning on terminal output:
gimp-3.0: GEGL-WARNING: gegl_node_connect: the sink node 'gegl:text 0x207914f0' doesn't have a pad named 'input', bailing.
This is because gegl:text
indeed does not have any input (if you
remember the list of subclasses of GeglOperation
, you may have guessed
that gegl:text
is a source operation), so linking with our input
node above is wrong.
You may think that a solution could be to link like this (disregarding the input):
static void
attach (GeglOperation *operation)
{
GeglNode *gegl = operation->node;
GeglNode *text;
GeglNode *output;
output = gegl_node_get_output_proxy (gegl, "output");
text = gegl_node_new_child (gegl,
"operation", "gegl:text",
"string", "Hello World!",
"size", 30.0,
NULL);
gegl_node_link_many (text, output, NULL);
}
… but even though it kinda works, it also behaves a bit weird. Let’s
rather say that what we want our operation to do is to write the text
over the current input image. It will also allow to have a slightly more
complex graph. For this, we will use svg:src-over
which implements the
standard Porter-Duff operation “over”. In other words, it will composite
the input
node with the text
node:
static void
attach (GeglOperation *operation)
{
GeglNode *gegl = operation->node;
GeglNode *input;
GeglNode *output;
GeglNode *text;
GeglNode *over;
input = gegl_node_get_input_proxy (gegl, "input");
output = gegl_node_get_output_proxy (gegl, "output");
text = gegl_node_new_child (gegl,
"operation", "gegl:text",
"string", "Hello World!",
"size", 30.0,
NULL);
over = gegl_node_new_child (gegl, "operation", "svg:src-over", NULL);
gegl_node_link_many (input, over, output, NULL);
gegl_node_connect (text, "output", over, "aux");
}
The following graph is created:
input ----> src-over ----> output
^
|
text -------´
Adding Operation Properties
Same as adding arguments to a plug-in procedure was giving a whole new dimension to your plug-in so would adding properties to your operation.
We finally get to fill the initial GEGL_PROPERTIES
section:
#ifdef GEGL_PROPERTIES
property_string (text, "Text", "Hello World!")
description ("The text to display on canvas")
ui_meta ("multiline", "true")
property_double (font_size, "Size", 30.0)
description ("Font size in pixels.")
value_range (0.0, 2048.0)
ui_meta ("unit", "pixel-distance")
#else
Then you want to bind your “text” and “font-size” newly created
properties to respectively the “string” and “size” properties of
the text
node, like this:
static void
attach (GeglOperation *operation)
{
GeglNode *gegl = operation->node;
GeglNode *input;
GeglNode *output;
GeglNode *text;
GeglNode *over;
input = gegl_node_get_input_proxy (gegl, "input");
output = gegl_node_get_output_proxy (gegl, "output");
text = gegl_node_new_child (gegl,
"operation", "gegl:text",
"string", "Hello World!",
"size", 30.0,
NULL);
over = gegl_node_new_child (gegl, "operation", "svg:src-over", NULL);
gegl_node_link_many (input, over, output, NULL);
gegl_node_connect (text, "output", over, "aux");
gegl_operation_meta_redirect (operation, "text", text, "string");
gegl_operation_meta_redirect (operation, "font-size", text, "size");
}
Note that this works like this because it is a simple case of mapping properties with the same meaning.
If the change were more complex, such as requiring to update the graph
itself, or if there is no simple property mapping between 2 nodes (e.g.
instead, you must compute property values from others), this is when
you’d implement the update()
method of the GeglOperationMetaClass
class.
Anyway, recompile, restart GIMP and test the operation. Tadaaaa!
You may feel free to write various text in the “Text” field, also change the size by dragging the slider and see how you get an instant feedback of the changes in your canvas. Exciting, right?
UI Metadata
In the property creation code, you may notice in particular the
value_range()
macro for double (or int) properties. You will also
notice ui_meta()
which is a way to set various keys and values which
GIMP knows how to make use of.
Here is an exhaustive list (at time of writing, at GIMP 3.0 release) of all the keys and values which GIMP understands so far:
- For all numeric properties (integer and floating point):
- “unit” key:
- “degree” value: some special wheel widget will be generated to allow choosing an angle more “instinctively” (the “Azimuth” property of “Emboss” effect has an example of it).
- “kelvin” value: a widget adapted to choosing temperatures, with an additional list of standard temperatures (see the “Color Temperature” effect for example).
- “pixel-coordinate” value: when 2 values have this metadata, with the additional “axis” key, one set to “x”, the other to “y”, it will create 2 sliders tied together with a button allowing to choose a coordinate by clicking directly on canvas (see for instance the “Grid” effect).
- “pixel-distance” value: when 2 values have this metadata, with the additional “axis” key, one set to “x”, the other to “y”, it will create 2 sliders tied together (see for instance the X/Y parameters in “Drop Shadow” effect).
- “unit” key:
- For string properties:
- “multiline” to “true” will create a
GtkTextView
allowing multi-line text, instead of a single-line entry. - “error” to “true” will output an error message box with the text.
- “multiline” to “true” will create a
- For all properties:
- “sensitive” key will make the property widget sensitive depending on the name of another (boolean) property.
- “visible” key works the same as “sensitive” except it hides widgets totally instead of simply making them insensitive.
- “label” key updates the label of the property depending on specified rules.
- “description” key updates the longer description of the property depending on specified rules.
The last few properties feel like the most interesting. We may add later a tutorial section to describe the syntax for these, more in details.
Reimplementing the Conditional Sensitivity
For now, we will simply re-implement the conditional sensitivity (from plug-in tutorial) of the “font-size” property depending on the value of a new “compute-size” property.
First let’s add a boolean property:
property_boolean (compute_size, "Compute Ideal Size", TRUE)
description ("This option will compute a font size "
"so that the text optimally fills the whole canvas")
Now update the font-size
property so that it is only sensitive when
compute-size
is FALSE
:
property_double (font_size, "Size", 30.0)
description ("Font size in pixels.")
value_range (0.0, 2048.0)
ui_meta ("unit", "pixel-distance")
ui_meta ("sensitive", "! compute-size")
Finally let’s implement an update()
method (and change a bit the
attach()
)!
static gdouble
compute_size (GeglOperation *operation,
GeglRectangle *irect)
{
GeglProperties *o = GEGL_PROPERTIES (operation);
GeglNode *text = o->user_data;
GeglRectangle orect;
gdouble size;
orect = gegl_node_get_bounding_box (text);
gegl_node_get (text, "size", &size, NULL);
size *= (gdouble) (gint) (irect->width / orect.width);
return size;
}
static void
attach (GeglOperation *operation)
{
GeglProperties *o = GEGL_PROPERTIES (operation);
GeglNode *gegl = operation->node;
GeglNode *input;
GeglNode *output;
GeglNode *text;
GeglNode *over;
input = gegl_node_get_input_proxy (gegl, "input");
output = gegl_node_get_output_proxy (gegl, "output");
text = gegl_node_new_child (gegl, "operation", "gegl:text", NULL);
o->user_data = text;
over = gegl_node_new_child (gegl, "operation", "svg:src-over", NULL);
gegl_node_link_many (input, over, output, NULL);
gegl_node_connect (text, "output", over, "aux");
gegl_operation_meta_redirect (operation, "text", text, "string");
}
static void
update (GeglOperation *operation)
{
GeglProperties *o = GEGL_PROPERTIES (operation);
GeglNode *gegl = operation->node;
GeglNode *text = o->user_data;
GeglNode *input;
GeglRectangle irect;
input = gegl_node_get_input_proxy (gegl, "input");
irect = gegl_node_get_bounding_box (input);
if (o->compute_size && irect.width > 1)
{
gdouble size;
size = compute_size (operation, &irect);
gegl_node_set (text, "size", size, NULL);
}
else
{
gegl_node_set (text, "size", o->font_size, NULL);
}
}
static void
prepare (GeglOperation *operation)
{
static gboolean initialized = FALSE;
if (! initialized)
{
GeglNode *gegl = operation->node;
GeglRectangle irect;
GeglNode *input;
input = gegl_node_get_input_proxy (gegl, "input");
irect = gegl_node_get_bounding_box (input);
if (irect.width > 0)
{
update (operation);
initialized = TRUE;
}
}
}
static void
gegl_op_class_init (GeglOpClass *klass)
{
GeglOperationClass *operation_class = GEGL_OPERATION_CLASS (klass);
GeglOperationMetaClass *meta_class = GEGL_OPERATION_META_CLASS (klass);
operation_class->prepare = prepare;
operation_class->attach = attach;
meta_class->update = update;
gegl_operation_class_set_keys (operation_class,
"title", "Hello World Meta",
"name", "zemarmot:hello-world-meta",
"categories", "Artistic",
"description", "Hello World as a Meta filter for official GIMP tutorial",
NULL);
}
What I did here is to store the text node as user_data
on the
operation properties (note that you could store more data if needed, for
instance by allocating some struct
, keeping pointers to all info you
want to track, to be stored instead).
Then each time GEGL requires an update()
, we can set the proper text
properties.
Note that I am also implementing prepare()
, though only because the
input bounding box is still empty when attach()
is run. So we can’t
initialize properly the font size at the very start, if “compute-size”
is TRUE initially.
Now compile again, restart GIMP and test your filter again. You will notice a new “Compute Ideal Size” checkbox which not only make the font size slider insensitive, when checked, it also triggers a size computation so that the text fills the input layer.
- Back to “How to write a filter” tutorial index
- Previous tutorial: GEGL architecture within GIMP
- Next tutorial: Hello World as a Point Filter Operation