Writing a feature

Ok, so here we are, you have read the previous sections so you know roughly your way around Siril important files and variables.

It can also be worth taking a look at glib that we use a lot to handle all the machinery.

Remember that you will most probably need to write something that can handle:

  • single image or sequences

  • by command or through GUI

  • for mono or color images

  • for 16b or 32b images

but we'll get there.

Logging

All log output in Siril goes through the functions declared in src/core/siril_log.h. Each call is thread-safe (protected by com.mutex) and simultaneously writes to standard output (prefixed with log:), to the GUI log panel, and to the named pipe used by the CLI. Contributors should always use the semantic helpers rather than siril_log_color_message(), which is internal:

  • siril_log_message() -- neutral, default colour; use for general informational output.

  • siril_log_info() -- green; use to confirm that an operation succeeded.

  • siril_log_warning() -- salmon; use for non-fatal anomalies the user should be aware of.

  • siril_log_error() -- red; use when an operation fails.

  • siril_log_bold() -- bold; use for section headers or important milestones.

  • siril_log_status() -- blue; use for progress updates during a long operation.

  • siril_log_debug() ; use for debugging purposes.

All functions accept printf-style format strings and return a pointer to the formatted message (backed by a static buffer), which can be passed to the progress interface if needed. User-visible strings must be wrapped in _(), except for siril_log_debug().

Process a single image

We'll take as an example a feature which does a processing of some sort to an image.

The steps would be:

  • you create a new file (mynewproc.c and its header mynewproc.h) in the appropriate folder in src/, see what appropriate means below.

  • the first function in there should take a gpointer to a structure holding some arguments (one of which is the image you want to process). It should return an integer (0 for sucess, any other value for errors, can be used to display meaningful info). (Actually, the function signature can be up to you as this will be called either from a generic_image_worker image hook or from a generic_sequence_worker image hook.)

gpointer mynewproc(gpointer p) {
    struct mynewproc_data *args = (struct mynewproc_data*) p;
    //do anything you want to the pixels of args->fit there
    return GINT_TO_POINTER(0);
}

A few things to remember:

  • you will need to deal in most cases with 2 bitdepths. Siril handles both 16b (or lower) as ushort and 32b as float. Data for these types are stored in data (pdata for pointer access) and fdata (fpdata for pointer access) members of the fit structure, for 16b and 32b respectively. Here-below an extract of their definitions:

data_type type;        // use of data or fdata is managed by this
WORD *data;            // 16-bit image data (depending on image type)
WORD *pdata[3];        // pointers on data, per layer data access (RGB)
float *fdata;          // same with float
float *fpdata[3];      // same with float

with data_type defined as:

typedef enum { DATA_USHORT, DATA_FLOAT, DATA_UNSUPPORTED } data_type;
  • you will also need to deal with mono and color images. To determine this, test fit->naxes[2] value (1 is mono, 3 is color and anything alse should through an error code). You can use g_assert statements at the begining of your function.

  • the mynewproc_data structure should be defined in mynewproc.h and contain all the inputs (and possibly outputs) required. Let's say for intance we want to develop a feature that multiply all pixels values by a factor f. For 32b image, we could also add an optional argument clamp to specifiy that we want to clamp the output to the [0,1] range.

Warning

The first element in the structure MUST be a destructor, by convention called destroy_fn. You must define an allocator function for your struct, which must use calloc() to allocate the struct and if the struct contains dynamically allocated members it must set the destroy_fn (which you must also define, and which must free all dynamically allocated memory owned by the struct). If and only if the struct contains no dynamically allocated members, no destroy_fn is needed and the struct can be allocated simply using calloc(). (If destroy_fn is NULL then the struct itself will be freed using free()).

You could have in mynewproc.h the following:

// arg structure definition
struct mynewproc_data {
    destructor destroy_fn;
    fits *fit; // image to process, replaced when the process is successful
    sequence *seq; // sequence to process
    gchar *seqprefix; // prefix to add to sequence
    float f; // factor
    gboolean clamp; // flag to clamp in the [0,1] range for 32b images
    threading_type threads; // for multithreading purposes
};
// single-image processing declaration
gpointer mynewproc(gpointer p);

Note

Dealing with sequences and multithreading is explained a bit later, but added there to introduce the concept.

  • It is most time easier to write 2 static functions for handling the 2 bitdepths and then call them from the main processing function:

static gpointer mynewproc_ushort(gpointer p) {
// handle the ushort case here
}

static gpointer mynewproc_float(gpointer p) {
// handle the float case here
}

gpointer mynewproc(gpointer p) {
    struct mynewproc_data *args = (struct mynewproc_data*) p;
    if (args->fit->type == DATA_USHORT) {
        retval = mynewproc_ushort(args);
    } else if (args->fit->type == DATA_FLOAT) {
        retval = mynewproc_float(args);
    }
    return GINT_TO_POINTER(retval);
}

Warning

Keep in mind that your processing should honor com.pref.force_16bit setting or notify the user that it can't (if there's a very good reason). If your processing requires floating point maths then you will probably need to convert to float in order to do the calculations, but honouring com.pref.force_16bit means turning the output float data back into WORD data.

Write the command for a single image

Now that you have your processing function, it is time to write the command to test it.

You will need to modify 4 files:

  • in command.c, add a function process_mynewproc(int nb) and most probably a static parser, like parse_mynewproc_arg (we'll see why when we get to write the sequence equivalent).

  • in command.h, declare process_mynewproc(int nb)

  • in command_def.h, define a string, say STR_MYNEWPROC, that will be displayed as the command tooltip. Guideline for formatting the string is given there

  • in command_list.h, add your function process_mynewproc together with the necessary arguments:

{"mynewproc", 1, "mynewproc factor [-clamp]", process_mynewproc, STR_MYNEWPROC, TRUE, REQ_CMD_SINGLE_IMAGE}

The arguments are:

  • the name of the command,

  • the minimum number of arguments that should be passed. For our example, we need to pass at least the factor by which we want to scale the image,

  • the command syntax. Mandatory arguments are written just after the name of the command, optional arguments are written between [], choices are enclosed between {} with a | separating the possible values.

  • the name of the function which is to be called

  • the name of its tooltip string as defined in command_def.h

  • if the command can be used in a script (in most cases, it should)

  • conditions that need to be met for the command to be evaluated. More than one value can be added separating them with bitwise logical operators as required. The acceptable values are listed in siril.h:cmd_prerequires enum. For instance here, it means the command requires a single image is loaded. Otherwise the command preprocessor won't even bother trying to evaluate the command.

Warning

In all these files, please add your functions respecting the alphabetical order!!!

Now what does the function process_mynewproc should do:

  • parse the arguments

  • check for their consistency

  • create a generic_img_args struct and an image-specific params struct

  • launch the function generic_image_worker() in the designated processing thread

  • deal with the errors along the process and return a status from cmd_errors enum

Arguments are passed to this function by means of a static null-terminated array word (of size 50, but this is in no possible way an invitation to write a command that takes 49 arguments!). The first value, word[0] is the name of the command, the following ones are the arguments that should be parsed untill reaching null string.

Writing a parser here may seem a bit like an overkill. But the point is to make the command parsing functional for both image and sequence processing. In this way, we avoid duplicating code and it makes it easier to review and maintain (adding new options, correcting bugs etc...)

Here's the code to add in command.c:

int mynewproc_image_hook(struct generic_img_args *args, fits *fit, int nb_threads) {
    // This function is a wrapper to the real image processing function - it exists as
    // the generic_image_worker image hooks require different parameters to the
    // generic_sequence_worker image hooks, and each will call the actual processing
    // function.
    return GPOINTER_TO_INT(mynewproc(args->user));
}

static cmd_errors parse_mynewproc_args(int start, int nb, struct mynewproc_data *arg) {
    for (int i = start; i < nb; i++) {
        if (i == start) { // first positional argument: factor
            gchar *end;
            args->factor = g_ascii_strtod(word[i], &end);
            if (end == word[i]) {
                siril_log_message(_("Invalid argument %s, aborting.\n"), word[i]);
                free(args);
                return CMD_ARG_ERROR;
            }
        } else if (!g_strcmp0(word[i], "-clamp")) { // optional argument clamp
            args->clamp = TRUE;
        } else {
            siril_log_error(_("Unknown parameter %s, aborting.\n"), word[i]);
            free(args);
            return CMD_ARG_ERROR;
        }
    }
    return CMD_OK;
}

int process_mynewproc(int nb) {
    struct mynewproc_data *params = malloc(1, sizeof(mynewproc_data));
    // initialize defaults
    params->factor = 1.f;
    params->clamp = FALSE;
    params->threads = MULTI_THREADED;
    params->fit = gfit;
    params->seq = NULL;
    params->seqprefix = NULL;

    int retval = parse_mynewproc_args(1, nb, params);
    if (!retval)
        return retval;

    retval = check_mynewproc_args(params);
    if (!retval)
        return retval;

    // Allocate worker args
    struct generic_img_args *args = calloc(1, sizeof(struct generic_img_args));
    if (!args) {
        PRINT_ALLOC_ERR;
        free_mirror_args(params);
        return CMD_ALLOC_ERROR;
    }

    args->fit = gfit;
    args->mem_ratio = 1.0f; // Accounts for how many times the original image size memory is required during processing
    args->image_hook = mynewproc_image_hook; // An image hook for your processing function
    args->mask_hook = mynewproc_mask_hook; // Only required if mynewproc changes geomerty or otherwise requires changes to the image msk
    args->idle_function = NULL;  // Not required for command-line interpreting functions, but is required to handle GUI
                                 // updates when called from the GUI where you would have ``end_mynewproc``
    args->description = _("Describe my process");
    args->verbose = TRUE;
    args->user = params; // Plug in your processing-specific parameters struct here
    args->max_threads = com.max_thread; // Generally for single image processing you want to allow maximum multithreading
    args->command_updates_gfit = TRUE; // Set TRUE if the command updates the main image (most do!)
    args->command = TRUE; // Set this TRUE to distinguish between command-line functions and functions called from the GUI

    if (!start_in_new_thread(generic_image_worker, args)) { // Starts the generic_image_worker
        free_generic_img_args(args); // If there is an error we free the args - this automatically frees params as well, using the destroy_fn
        return CMD_GENERIC_ERROR;
    }

    return CMD_OK;
}

and in mynewproc.c:

/* argument sanity check function
*/
int check_mynewproc_args(args) {
    if (args->clamp && (com.pref.force_16bit))
        siril_log_message(_("Passing the option -clamp has no effect when output is in 16b, ignoring.\n"));
    // more checks to come when we'll deal with sequences
    return CMD_OK;
}

/* end function to deal with GUI if needed
*/
gboolean end_mynewproc(gpointer p) {
    struct mynewproc_data *args = (struct mynewproc_data*) p;
    notify_gfit_modified();
    free(args);
    return FALSE;
}

gchar *mynewproc_log_hook(gpointer p, log_hook_detail detail) {
    // This function returns a gchar* describing what has been done: the params are passed
    // as p so they can be used to include parameters in the description. For functions
    // requiring a lengthy description, a summary should also be provided if detail == SUMMARY.
    struct mynewproc_data *params = (struct mynewproc_data *) p;
    gchar *msg = NULL;
    if (detail == SUMMARY) {
        msg = g_strdup_printf(_("Basic summary of my process, parameter = %f"), params->parameter1);
    } else {
        msg = g_strdup_printf(_("Detailed description of my process, parameter = %f, ..."), params->parameter1);
    }
    return msg;
}

Many things to say, so we'll go through all the new stuff step by step:

  • Having both parse_mynewproc_args and check_mynewproc_args is really an overkill in the very simple example we're looking at.

  • The whole philosophy is:
    • parse the arguments (check their types and possibly that they are in the right range for numerical values) in the parser

    • check their compatibility, when there is more than one option obviously, and that some are not compatible together in the checker. Otherwise, the parser becomes very quickly a mess, both to write, to test and to review. Then it is up to you to decide whether failing one the checks should abort the command or just throw a warning.

  • There are many more examples on how to deal with different arguments in command.c. Useful functions that come to` mind are g_ascii_strtoull for integers conversion, g_str_has_prefix for options in the form -opt=....

  • siril_log_message, siril_log_error, siril_log_warnings, siril_log_info, siril_log_status and siril_log_debug are thread-safe functions to write in the Console/CLI stdout. They must always be written as translatable strings using the syntax _("my message here") with the preceding _ and the string enclosed in (). Errors should be written in red to be very visible in the Console.

  • Please be kind to our dear translators and make your console outputs as generic as possible, possibly breaking them down in smaller snippets. e.g. use something like:

siril_log_error(_("Mynewproc error: value of %s argument should be in the range %s, aborting\n"), "val1", "[-1,1]");
siril_log_error(_("Mynewproc error: value of %s argument should be in the range %s, aborting\n"), "val2", "[0,100]");

if you have many arguments that can share a common message.

  • If you just need to output something for debugging purposes, use siril_debug_print instead. The string must not be translatable.

  • In 1.5.x set_cursor_waiting() and show_time() are handled by the generic_image_worker, so there is no longer any need to use these in your processing function or command handler (in fact if you do there will be duplicate messages!)

  • Siril is written in C so we need to free along the way to avoid leakage. While we absolutely not encourage the use of goto: which makes the code really hard to follow, we acknowledge the fact (and so we do it) that it makes it easier to deal with errors checking, in particular in the parser. Remember that once your params is added to the generic_img_args, the entire thing can be efficiently and reliably freed using free_generic_img_args().

  • one last very important warning:

Warning

Any change to some graphical display, UI element, label update, no matter how tiny it is, should be done in an idle function.

Write the command for a sequence

Once you are happy with your processing behaving as intended on a single image, it is time to write the processing for a whole sequence. To that end, we will use the machinery that is set in core/processing.c.

The first thing you will need to do is to add this function to mynewproc.c:

void apply_mynewproc_to_sequence(struct mynewproc_data *mynewproc_args) {
    struct generic_seq_args *args = create_default_seqargs(mynewproc_args->seq);
    args->filtering_criterion = seq_filter_included;
    args->nb_filtered_images = args->seq->selnum;
    args->prepare_hook = seq_prepare_hook;
    args->image_hook = mynewproc_image_hook;
    args->stop_on_error = FALSE;
    args->description = _("Mynewproc");
    args->has_output = TRUE;
    args->parrallel = TRUE;
    args->output_type = get_data_type(args->seq->bitpix);
    args->new_seq_prefix = mynewproc_args->seqprefix;
    args->load_new_sequence = TRUE;
    args->user = mynewproc_args;
    mynewproc_args->fit = NULL;
    start_in_new_thread(generic_sequence_worker, args);
}

The generic sequence processor generic_sequence_worker can be fine-tuned with many different functions depending on what needs to be done:

  • the compute_size_hook computes the output sequence size (if has_output member is TRUE). If NULL, the generic function assumes the size of an output image is the same as an input image.

  • the compute_mem_limits_hook computes how many threads can be run in parrallel.

  • the prepare_hook here calls a generic implementation that tidies existing sequences and images. This is useful when your process creates an output sequence. You can have a more specific implementation if required, by passing a function here. Or NULL is nothing is to be prepared.

  • the image_hook is probably the core function of the process, as it is the one which effectively applies your process on each image.

  • the save_hook (not specified here so handled by the genric implementation) saves the output images.

  • the finalize_hook finishes the business once the processing of the sequence has taken place, like aggregating and saving results

  • the idle_function (not specified here) handles special cases to be handled in the idle phase.

All your hooks should be named with the convention mynewproc_hookname so that it's easier for everybody to understand the intention.

The function to apply to a sequence should also follow the convention apply_mynewproc_to_sequence.

Once this is done, we can proceed to writing the related command in command.c, updating the parser and defining a new function:

static cmd_errors parse_mynewproc_args(int start, int nb, struct mynewproc_data *arg) {
    for (int i = start; i < nb; i++) {
        if (i == start) { // first positional argument: factor
            gchar *end;
            args->factor = g_ascii_strtod(word[i], &end);
            if (end == word[i]) {
                siril_log_message(_("Invalid argument %s, aborting.\n"), word[i]);
                free(args);
                return CMD_ARG_ERROR;
            }
        } else if (!g_strcmp0(word[i], "-clamp")) { // optional argument clamp
            args->clamp = TRUE;
        } else if (g_str_has_prefix(word[i], "-prefix=")) { // optional argument -prefix= for sequences
            char *current = word[i], *value;
            value = current + 8;
            if (value[0] == '\0') {
                siril_log_message(_("Missing argument to %s, aborting.\n"), current);
                free(args);
                return CMD_ARG_ERROR;
            }
            free(args->seqprefix); // we free as it was set to default value
            args->seqprefix = strdup(value);
        } else {
            siril_log_error(_("Unknown parameter %s, aborting.\n"), word[i]);
            free(args);
            return CMD_ARG_ERROR;
        }
    }
    return CMD_OK;
}

int process_seq_mynewproc(int nb) {
    // check that we can load the sequence
    sequence *seq = load_sequence(word[1], NULL);
    if (!seq)
        return CMD_SEQUENCE_NOT_FOUND;
    // check if the sequence we are about to process is the one loaded in the GUI
    // this has no effect when in CLI mode
    if (check_seq_is_comseq(seq)) {
        free_sequence(seq, TRUE);
        seq = &com.seq;
    }
    struct mynewproc_data *args = malloc(1, sizeof(mynewproc_data));
    // initialize defaults
    args->factor = 1.f;
    args->clamp = FALSE;
    args->threads = com.max_thread;
    args->fit = NULL;
    args->seq = seq;
    args->seqprefix = "mnp_"; // "mnp" being short for MyNewProcess

    int retval = parse_mynewproc_args(2, nb, args); // we start at 2 here because word[1] is the sequence name
    if (!retval)
        return retval;

    retval = check_mynewproc_args(args);
    if (!retval)
        return retval;

    apply_mynewproc_to_sequence(args);
    return CMD_OK;
}

We also update the sanity-check function in mynewproc.c (still not instrumental):

/* argument sanity check function
*/
int check_mynewproc_args(args) {
    if (args->clamp && (com.pref.force_16bit))
        siril_log_message(_("Passing the option -clamp has no effect when output is in 16b, ignoring.\n"));
    if (args->fit && args->seqprefix)
        siril_log_message(_("Passing the option -prefix= has no effect when working on a single image, ignoring.\n"));
    return CMD_OK;
}

And we add the new definition in command_list.h (together with creating a new definition for STR_SEQ_MYNEWPROC in command_def.h)

{"seqmynewproc", 2, "seqmynewproc seqname factor [-clamp] [-prefix=]", process_seq_mynewproc, STR_SEQ_MYNEWPROC, TRUE, REQ_CMD_NONE}

Add the GUI version

  • things about Glade and callbacks

  • things about Undo

  • things about com.headless and com.script