Hello World as a Point Filter Operation

Now let’s go in a completely different directly, much more raster-like. This time, we will implement a point filter operation.

Point Filter operations are a good choice when you don’t care about the surrounding pixels, i.e. that each pixel depends only on the pixel data at corresponding coordinates in the input.

My demo point filter operation will simply display a “H” in the middle of the input drawable, by inverting the color value linearly. It means we will also care about the pixel coordinates.

No-Op Point Filter Operation

But first of all, let’s create the most basic Point Filter operation possible:

#ifdef GEGL_PROPERTIES

#else

#define GEGL_OP_POINT_FILTER
#define GEGL_OP_NAME     gimp_tutorial_point_filter_op
#define GEGL_OP_C_SOURCE gimp-tutorial-point-filter-op.c

#include "gegl-op.h"

static gboolean
process (GeglOperation       *op,
         void                *in_buf,
         void                *out_buf,
         glong                n_pixels,
         const GeglRectangle *roi,
         gint                 level)
{
  memcpy (out_buf, in_buf, n_pixels * 4 * sizeof (gfloat));
}

static void
gegl_op_class_init (GeglOpClass *klass)
{
  GeglOperationClass            *operation_class    = GEGL_OPERATION_CLASS (klass);
  GeglOperationPointFilterClass *point_filter_class = GEGL_OPERATION_POINT_FILTER_CLASS (klass);

  point_filter_class->process = process;

  gegl_operation_class_set_keys (operation_class,
                                 "title",       "Hello World Point Filter",
                                 "name",        "zemarmot:hello-world-point-filter",
                                 "categories",  "Artistic",
                                 "description", "Hello World as a Point filter for official GIMP tutorial",
                                 NULL);
}

#endif

You will recognize the similar structure as our Meta Operation, except that GEGL_OP_POINT_FILTER replaces GEGL_OP_META. Also now we implement the process method of the GeglOperationPointFilterClass.

Note that the format of in_buf and out_buf is “RGBA float”, i.e. linear RGBA in floating point. This is the default format for point filter operations. We could override this if we wanted, by reimplementing the prepare() method of GeglOperationClass.

To make this a proper no-op operation, we just copy the input data into the output.

Processing the Input into the Output

NI will only process the input to produce some kind of “H” by inverting pixels in specific coordinates.

For this, we will only edit process():

static gboolean
process (GeglOperation       *op,
         void                *in_buf,
         void                *out_buf,
         glong                n_pixels,
         const GeglRectangle *roi,
         gint                 level)
{
  GeglNode       *gegl = op->node;
  GeglNode       *input;
  gfloat         *GEGL_ALIGNED in_pixel;
  gfloat         *GEGL_ALIGNED out_pixel;
  GeglRectangle   irect;
  glong           i;
  gboolean        invert = FALSE;
  gint            x      = roi->x;
  gint            y      = roi->y;

  input = gegl_node_get_input_proxy (gegl, "input");
  irect = gegl_node_get_bounding_box (input);

  in_pixel  = in_buf;
  out_pixel = out_buf;

  for (i = 0; i < n_pixels; i++)
    {
      if (y >= irect.height / 4 + irect.y && y <= 3 * irect.height / 4 + irect.y &&
          ((x >= irect.width / 3 - irect.width / 100 + irect.x && x <= irect.width / 3 + irect.width / 100 + irect.x)         ||
           (x >= 2 * irect.width / 3 - irect.width / 100 + irect.x && x <= 2 * irect.width / 3 + irect.width / 100 + irect.x) ||
           (x > irect.width / 3 + irect.width / 100 + irect.x && x < 2 * irect.width / 3 + irect.width / 100 + irect.x &&
            y >= irect.height / 2 - irect.height / 100 + irect.y && y <= irect.height / 2 + irect.height / 100 + irect.y)))
        invert = TRUE;
      else
        invert = FALSE;

      out_pixel[0] = invert ? 1.0 - in_pixel[0] : in_pixel[0];
      out_pixel[1] = invert ? 1.0 - in_pixel[1] : in_pixel[1];
      out_pixel[2] = invert ? 1.0 - in_pixel[2] : in_pixel[2];
      out_pixel[3] = in_pixel[3];
      in_pixel  += 4;
      out_pixel += 4;

      if (x + 1 >= roi->x + roi->width)
        {
          x = roi->x;
          y++;
        }
      else
        {
          x++;
        }
    }

  return TRUE;
}

This piece of code is interesting as it shows how you can use the ROI to figure out your coordinates on the image, which you can then compare to the input dimensions.

Then it shows how we change pixel values. Since we know that we are in floating point (from 0.0 to 1.0 within the color space, though you can go unbounded), every gfloat is a channel value, and for every pixel, we have 4 values one after the other (red, green, blue and finally alpha).

Changing the input and output color model, space or TRC

In this demo, we made the choice to work in linear, though it may not be the best choice. Say you decided to work in perceptual because inverting colors gave nicer results? As we said above, for this, we could reimplement the prepare() method of GeglOperationClass:

static void prepare
(GeglOperation *operation)
{
  const Babl *space = gegl_operation_get_source_space (operation, "input");

  gegl_operation_set_format (operation, "input", babl_format_with_space ("R~G~B~A float", space));
  gegl_operation_set_format (operation, "output", babl_format_with_space ("R~G~B~A float", space));
}

static void
gegl_op_class_init (GeglOpClass *klass)
{
  GeglOperationClass            *operation_class    = GEGL_OPERATION_CLASS (klass);
  GeglOperationPointFilterClass *point_filter_class = GEGL_OPERATION_POINT_FILTER_CLASS (klass);

  operation_class->prepare    = prepare;
  point_filter_class->process = process;

  gegl_operation_class_set_keys (operation_class,
                                 "title",       "Hello World Point Filter",
                                 "name",        "zemarmot:hello-world-point-filter",
                                 "categories",  "Artistic",
                                 "description", "Hello World as a Point filter for official GIMP tutorial",
                                 NULL);
}

What we are doing in prepare() here is simply telling GEGL that:

  • I want “input” pad data as RGBA in perceptual TRC (which means in fact using the sRGB curve in GEGL), but staying within my same color space.
  • I will return “output” pad data as RGBA perceptual, also within the same color space.

This is an extremely important step, because then you are able to make some assumption in your code. For instance in our process() code, we were already assuming that every pixel is made of 4 floating point channels (in this given order: red, then green, then blue, and finally alpha). It means that even if someone were inputting a grayscale or CMYK buffer, you would receive perceptual RGBA. Take this as a known fact. GEGL does all necessary intermediate conversion on your behalf, hence ensuring correct colors.

Note that in some cases, you may want to have conditional input format depending on real input format. For instance, if the input was CMYK, in some cases, making a round-trip to a RGB space may be counter-productive (though when processing artistic filters, changing models is often necessary).

For the “output” model and space, just advertize what you return and do not do any unecessary conversions yourself (in particular do not try to return to the original format). Indeed you don’t know what is the next node. Maybe there are more filters after you and they may request exactly the format you would output. If you were to convert it, maybe GEGL would convert it back for the next node, so an unnecessary round-trip conversion would have happened.

In other words: don’t try to be clever and let GEGL take care of conversions, unless you really know what you do.

Here is a comparative export of how our point filter Hello World renders when run on our GIMP 3.0 splash screen (by Sevenix, CC by-sa 4.0) with exactly same process() code, except that the left version is processed on linear space and right version on perceptual space:

GIMP 3.0 splash screen with our point-filter demo run in linear and
perceptual

A bit on Color Spaces in babl

Regarding color models and spaces, GEGL itself relies on babl, our pixel encoding and color space conversion engine, also born from the GIMP project.

babl supports many color models (RGB, CMYK, grayscale, indexed images, CIE Lab, HSV, HSL… and just too many more to cite them all in this tutorial), with or without alpha channel, with various channel types (integers of various sizes, floating point in half, single or double…), using any possible TRC.

I will not dive extremely deep on this topic, but I just wanted to give a bit of insight on some of its syntax, in particular regarding RGB. In the context of babl and GEGL, when you read:

  • RGB: this is linear RGB (bypassing any TRC which may be contained in the color profile but still preserving its chromaticities).
  • R~G~B~: this is perceptual RGB (again bypassing color space, yet preserving chromaticities).
  • R'G'B': this is the RGB space following TRC and chromaticities from the space attached to it.

Note that all colors have a space attached to it by definition. Simply when it is not explicitly set, a default is used. For RGB formats in particular, the default is the sRGB space.

For this reason, it is better practice to always define color formats with babl_format_with_space(). If you wish to use the default space, set NULL as second argument. This is preferred to using the legacy babl_format() since you make it explicit that you chose the default space on purpose.

In our above code, what we did is deciding to work on perceptual RGB, in floating point, while still keeping the source image’s space chromaticities.

Of course, this is mostly true for relative color models (such as CMYK, RGB, HSV, HSL…). For absolute color models (where model and space are indissociable), such as CIE L*a*b*, attaching a space won’t actually change the format yet it is an interesting trick because it allows round-trips while preserving space information.

Conclusion

I am not going to add any operation property this time as I assume you got how it worked. But I leave doing this as an exercise.

Of course since we hardcoded the position to decide which pixel to edit, changing the text is not a good property anymore, but you could add some properties to tweak the size, position and ratio of your ‘H’ for instance.