Ctrl K
ComunidadGuías…

src/iop/rasterfile.c

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
    This file is part of darktable,
    Copyright (C) 2025-2026 darktable developers.

    darktable is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    darktable is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "bauhaus/bauhaus.h"
#include "develop/tiling.h"
#include "develop/develop.h"
#include "develop/imageop.h"
#include "develop/imageop_gui.h"
#include "develop/imageop_math.h"
#include "common/interpolation.h"
#include "common/fast_guided_filter.h"
#include "common/dtdata.h"
#include "common/pfm.h"
#include "common/ras2vect.h"
#include "common/utility.h"
#include "imageio/imageio_png.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"

#ifdef _OPENMP
#include <omp.h>
#endif

#define SET_THRESHOLD 0.6f

DT_MODULE_INTROSPECTION(2, dt_iop_rasterfile_params_t)

typedef enum dt_iop_rasterfile_mode_t
{
  DT_RASTERFILE_MODE_ALL = 7,     // $DESCRIPTION: "all RGB channels"
  DT_RASTERFILE_MODE_RED = 1,     // $DESCRIPTION: "only red"
  DT_RASTERFILE_MODE_GREEN = 2,   // $DESCRIPTION: "only green"
  DT_RASTERFILE_MODE_BLUE = 4,   // $DESCRIPTION: "only blue"
  DT_RASTERFILE_MODE_REDGREEN = DT_RASTERFILE_MODE_RED | DT_RASTERFILE_MODE_GREEN, // $DESCRIPTION: "red and green"
  DT_RASTERFILE_MODE_REDBLUE = DT_RASTERFILE_MODE_RED | DT_RASTERFILE_MODE_BLUE, // $DESCRIPTION: "red and blue"
  DT_RASTERFILE_MODE_GREENBLUE = DT_RASTERFILE_MODE_GREEN | DT_RASTERFILE_MODE_BLUE, // $DESCRIPTION: "green and blue"
} dt_iop_rasterfile_mode_t;

#define RASTERFILE_MAXFILE 2048

typedef struct dt_iop_rasterfile_params_t
{
  dt_iop_rasterfile_mode_t mode;  // $DEFAULT: DT_RASTERFILE_MODE_ALL $DESCRIPTION: "mode"
  // v1: a file in the raster mask root folder. still honored when set and
  // no sidecar entry is referenced
  char path[RASTERFILE_MAXFILE];
  char file[RASTERFILE_MAXFILE];
  // v2: an entry in the image's .dtdata sidecar, the fields of
  // dt_dtdata_ref_t laid out inline so introspection can see them
  char entry[DT_DTDATA_ENTRY_LEN];
  int entry_kind;
  int entry_origin;
  int entry_width;
  int entry_height;
  int entry_bpc;
  char entry_producer[DT_DTDATA_PRODUCER_LEN];
} dt_iop_rasterfile_params_t;

typedef struct dt_iop_rasterfile_data_t
{
  dt_iop_rasterfile_mode_t mode;
  char filepath[PATH_MAX];
  dt_dtdata_ref_t ref;
} dt_iop_rasterfile_data_t;

typedef struct dt_rasterfile_cache_t
{
  dt_pthread_mutex_t lock;  // all access to cache data MUST be done in mutex locked state !
  volatile dt_hash_t hash;  // as the hash is shared between threads make sure it's actually read from mem
  int width, height;
  float *volatile mask;
} dt_rasterfile_cache_t;

const char *name()
{
  return _("external raster masks");
}

const char *aliases()
{
  return _("raster|mask");
}

const char **description(dt_iop_module_t *self)
{
  return dt_iop_set_description(self,
      _("read PFM/PNG files recorded for use as raster masks"),
      _("corrective or creative"),
      _("linear, raw, scene-referred"),
      _("linear, raw"),
      _("linear, raw, scene-referred"));
}

int default_group()
{
  return IOP_GROUP_BASIC | IOP_GROUP_TECHNICAL;
}

int flags()
{
  return IOP_FLAGS_WRITE_RASTER;
}

dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,
                                            dt_dev_pixelpipe_t *pipe,
                                            dt_dev_pixelpipe_iop_t *piece)
{
  return (pipe && !dt_image_is_raw(&pipe->image)) ? IOP_CS_RGB : IOP_CS_RAW;
}

typedef struct dt_iop_rasterfile_gui_data_t
{
  GtkWidget *mode;
  GtkWidget *import;
  GtkWidget *info;
  GtkWidget *vectorize;
} dt_iop_rasterfile_gui_data_t;

int legacy_params(dt_iop_module_t *self,
                  const void *const old_params,
                  const int old_version,
                  void **new_params,
                  int32_t *new_params_size,
                  int *new_version)
{
  typedef struct dt_iop_rasterfile_params_v2_t
  {
    dt_iop_rasterfile_mode_t mode;
    char path[RASTERFILE_MAXFILE];
    char file[RASTERFILE_MAXFILE];
    char entry[DT_DTDATA_ENTRY_LEN];
    int entry_kind;
    int entry_origin;
    int entry_width;
    int entry_height;
    int entry_bpc;
    char entry_producer[DT_DTDATA_PRODUCER_LEN];
  } dt_iop_rasterfile_params_v2_t;

  if(old_version == 1)
  {
    typedef struct dt_iop_rasterfile_params_v1_t
    {
      dt_iop_rasterfile_mode_t mode;
      char path[RASTERFILE_MAXFILE];
      char file[RASTERFILE_MAXFILE];
    } dt_iop_rasterfile_params_v1_t;

    const dt_iop_rasterfile_params_v1_t *o = old_params;
    dt_iop_rasterfile_params_v2_t *n = calloc(1, sizeof(dt_iop_rasterfile_params_v2_t));
    memcpy(n, o, sizeof(dt_iop_rasterfile_params_v1_t));
    *new_params = n;
    *new_params_size = sizeof(dt_iop_rasterfile_params_v2_t);
    *new_version = 2;
    return 0;
  }
  return 1;
}

static gboolean _has_source(const dt_iop_rasterfile_params_t *p)
{
  return p->entry[0] || (p->path[0] && p->file[0]);
}

static void _params_from_ref(dt_iop_rasterfile_params_t *p, const dt_dtdata_ref_t *ref)
{
  g_strlcpy(p->entry, ref->entry, sizeof(p->entry));
  p->entry_kind = ref->kind;
  p->entry_origin = ref->origin;
  p->entry_width = ref->width;
  p->entry_height = ref->height;
  p->entry_bpc = ref->bpc;
  g_strlcpy(p->entry_producer, ref->producer, sizeof(p->entry_producer));
  // a sidecar entry replaces a folder file
  memset(p->path, 0, sizeof(p->path));
  memset(p->file, 0, sizeof(p->file));
}

static void _ref_from_params(const dt_iop_rasterfile_params_t *p, dt_dtdata_ref_t *ref)
{
  memset(ref, 0, sizeof(*ref));
  g_strlcpy(ref->entry, p->entry, sizeof(ref->entry));
  ref->kind = p->entry_kind;
  ref->origin = p->entry_origin;
  ref->width = p->entry_width;
  ref->height = p->entry_height;
  ref->bpc = p->entry_bpc;
  g_strlcpy(ref->producer, p->entry_producer, sizeof(ref->producer));
}

void modify_roi_in(dt_iop_module_t *self,
                   dt_dev_pixelpipe_iop_t *piece,
                   const dt_iop_roi_t *const roi_out,
                   dt_iop_roi_t *roi_in)
{
  *roi_in = *roi_out;
  roi_in->scale = 1.0f;
  roi_in->x = 0;
  roi_in->y = 0;
  roi_in->width = piece->buf_in.width;
  roi_in->height = piece->buf_in.height;
}

static void _vectorize_button_clicked(GtkWidget *widget,
                                      dt_iop_module_t *self)
{
  dt_develop_t *dev = darktable.develop;

  dt_rasterfile_cache_t *cd = self->data;

  dt_pthread_mutex_lock(&cd->lock);

  const dt_image_t *const image = &(self->dev->image_storage);
  GList *forms = ras2forms(cd->mask, cd->width, cd->height, image,
                           SET_THRESHOLD, 0, 0.0, NULL);

  dt_pthread_mutex_unlock(&cd->lock);

  const int nbform = g_list_length(forms);
  if(nbform == 0)
  {
    dt_control_log(_("no mask extracted from the raster file\n"
                     "make sure the masks have proper contrast"));
  }
  else
  {
    dt_control_log(ngettext("%d mask extracted from the raster file",
                            "%d masks extracted from the raster file", nbform), nbform);

    // add all forms into the mask manager

    dt_masks_register_forms(dev, self, forms);
  }
}

// sbpc, when given, is the depth worth storing: 16 for a 16-bit PNG or a
// float PFM, 8 otherwise
static float *_read_rasterfile(char *filename,
                               const dt_iop_rasterfile_mode_t mode,
                               int *swidth,
                               int *sheight,
                               int *sbpc)
{
  *swidth = 0;
  *sheight = 0;
  if(sbpc) *sbpc = 8;
  if(!filename || filename[0] == 0) return NULL;

  const char *extension = g_strrstr(filename, ".");
  const gboolean is_png = extension && !g_ascii_strcasecmp(extension, ".png");

  if(is_png)
  {
    dt_imageio_png_t png;
    if(!dt_imageio_png_read_header(filename, &png))
    {
      dt_print(DT_DEBUG_ALWAYS, "failed to read PNG header from '%s'", filename ? filename : "???");
      dt_control_log(_("can't read raster mask file '%s'"), filename ? filename : "???");
      return NULL;
    }

    const size_t rowbytes = png_get_rowbytes(png.png_ptr, png.info_ptr);
    uint8_t *buf = dt_alloc_aligned((size_t)png.height * rowbytes);
    if(!buf)
    {
      fclose(png.f);
      png_destroy_read_struct(&png.png_ptr, &png.info_ptr, NULL);
      dt_print(DT_DEBUG_ALWAYS, "can't read raster mask file '%s'", filename ? filename : "???");
      dt_control_log(_("can't read raster mask file '%s'"), filename ? filename : "???");
      return NULL;
    }

    if(!dt_imageio_png_read_image(&png, buf))
    {
      dt_free_align(buf);
      dt_print(DT_DEBUG_ALWAYS, "can't read raster mask file '%s'", filename ? filename : "???");
      dt_control_log(_("can't read raster mask file '%s'"), filename ? filename : "???");
      return NULL;
    }

    const int width = png.width;
    const int height = png.height;
    float *mask = dt_iop_image_alloc(width, height, 1);
    if(!mask)
    {
      dt_free_align(buf);
      dt_print(DT_DEBUG_ALWAYS, "can't read raster mask file '%s'", filename ? filename : "???");
      dt_control_log(_("can't read raster mask file '%s'"), filename ? filename : "???");
      return NULL;
    }

    if(png.bit_depth < 16)
    {
      const float normalizer = 1.0f / 255.0f;
      DT_OMP_FOR()
      for(size_t k = 0; k < (size_t)width * height; k++)
      {
        const size_t base = 3 * k;
        float val = 0.0f;
        if(mode & DT_RASTERFILE_MODE_RED)   val = MAX(val, buf[base] * normalizer);
        if(mode & DT_RASTERFILE_MODE_GREEN) val = MAX(val, buf[base + 1] * normalizer);
        if(mode & DT_RASTERFILE_MODE_BLUE)  val = MAX(val, buf[base + 2] * normalizer);
        mask[k] = CLIP(val);
      }
    }
    else
    {
      const float normalizer = 1.0f / 65535.0f;
      DT_OMP_FOR()
      for(size_t k = 0; k < (size_t)width * height; k++)
      {
        const size_t base = 6 * k;
        const float red = (buf[base] * 256.0f + buf[base + 1]) * normalizer;
        const float green = (buf[base + 2] * 256.0f + buf[base + 3]) * normalizer;
        const float blue = (buf[base + 4] * 256.0f + buf[base + 5]) * normalizer;
        float val = 0.0f;
        if(mode & DT_RASTERFILE_MODE_RED)   val = MAX(val, red);
        if(mode & DT_RASTERFILE_MODE_GREEN) val = MAX(val, green);
        if(mode & DT_RASTERFILE_MODE_BLUE)  val = MAX(val, blue);
        mask[k] = CLIP(val);
      }
    }

    dt_free_align(buf);
    *swidth = width;
    *sheight = height;
    if(sbpc) *sbpc = png.bit_depth == 16 ? 16 : 8;
    return mask;
  }

  int width, height, channels, error = 0;
  float *image = dt_read_pfm(filename, &error, &width, &height, &channels, 3);
  float *mask = dt_iop_image_alloc(width, height, 1);
  if(!image || !mask)
  {
    dt_print(DT_DEBUG_ALWAYS,
             "can't read raster mask file '%s'", filename ? filename : "???");
    dt_control_log(_("can't read raster mask file '%s'"), filename ? filename : "???");

    dt_free_align(image);
    dt_free_align(mask);
    return NULL;
  }

  DT_OMP_FOR()
  for(size_t k = 0; k < (size_t)width * height; k++)
  {
    float val = 0.0f;
    if(mode & DT_RASTERFILE_MODE_RED)   val = MAX(val, image[k*3]);
    if(mode & DT_RASTERFILE_MODE_GREEN) val = MAX(val, image[k*3+1]);
    if(mode & DT_RASTERFILE_MODE_BLUE)  val = MAX(val, image[k*3+2]);
    mask[k] = CLIP(val);
  }

  *swidth = width;
  *sheight = height;
  if(sbpc) *sbpc = 16;
  dt_free_align(image);
  return mask;
}

// what this instance references: its sidecar entry, the folder file of a
// v1 edit, or nothing
static void _update_info(dt_iop_module_t *self)
{
  dt_iop_rasterfile_gui_data_t *g = self->gui_data;
  dt_iop_rasterfile_params_t *p = self->params;
  gchar *text = NULL;

  if(p->entry[0])
  {
    const char *dash = strchr(p->entry, '-');
    GList *entries = self->dev ? dt_dtdata_list_entries(self->dev->image_storage.id) : NULL;
    const gboolean present = g_list_find_custom(entries, p->entry, (GCompareFunc)g_strcmp0) != NULL;
    g_list_free_full(entries, g_free);
    if(p->entry_width > 0 && p->entry_height > 0)
      text = g_strdup_printf(_("mask %.8s, %d x %d"), dash ? dash + 1 : p->entry,
                             p->entry_width, p->entry_height);
    else
      text = g_strdup_printf(_("mask %.8s"), dash ? dash + 1 : p->entry);
    if(!present)
    {
      gchar *full = g_strdup_printf("%s (%s)", text, _("missing from sidecar"));
      g_free(text);
      text = full;
    }
  }
  else if(p->path[0] && p->file[0])
    text = g_strdup_printf(_("folder: %s"), p->file);
  else
    text = g_strdup(_("no mask imported"));

  gtk_label_set_text(GTK_LABEL(g->info), text);
  g_free(text);
  gtk_widget_set_sensitive(g->vectorize, _has_source(p));
}

static GtkFileChooserNative *_mask_file_chooser(const char *folder)
{
  GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
  GtkFileChooserNative *filechooser = gtk_file_chooser_native_new(
        _("select raster mask file"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_OPEN,
        _("_select"), _("_cancel"));
  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(filechooser), FALSE);
  if(folder && *folder && g_file_test(folder, G_FILE_TEST_IS_DIR))
    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), folder);

  GtkFileFilter *filter = GTK_FILE_FILTER(gtk_file_filter_new());
  gtk_file_filter_add_pattern(filter, "*.pfm");
  gtk_file_filter_add_pattern(filter, "*.PFM");
  gtk_file_filter_add_pattern(filter, "*.png");
  gtk_file_filter_add_pattern(filter, "*.PNG");
  gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter);
  gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(filechooser), filter);
  return filechooser;
}

// sidecars off: the previous behavior, a file inside the raster mask root
// folder referenced by its path
static void _select_folder_file(dt_iop_module_t *self)
{
  dt_iop_rasterfile_params_t *p = self->params;

  gchar *mfolder = dt_conf_get_string("plugins/darkroom/segments/def_path");
  if(!mfolder || !*mfolder)
  {
    dt_control_log(_("raster mask files root folder not defined"));
    g_free(mfolder);
    return;
  }

  GtkFileChooserNative *filechooser = _mask_file_chooser(mfolder);
  if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
  {
    gchar *filepath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
    const gboolean within = (strlen(filepath) > strlen(mfolder))
                         && (memcmp(filepath, mfolder, strlen(mfolder)) == 0);
    if(within)
    {
      char *relativepath = g_path_get_dirname(filepath);
      dt_strlcpy_to_fixed(p->path, relativepath, sizeof(p->path));
      g_free(relativepath);
      char *bname = g_path_get_basename(filepath);
      dt_strlcpy_to_fixed(p->file, bname, sizeof(p->file));
      g_free(bname);
      memset(p->entry, 0, sizeof(p->entry));
      _update_info(self);
      dt_dev_add_history_item(darktable.develop, self, TRUE);
    }
    else
      dt_control_log(_("selected file not within raster masks root folder"));
    g_free(filepath);
  }
  g_free(mfolder);
  g_object_unref(filechooser);
}

// sidecars on: read any PNG or PFM, reduce it to gray with the channel
// mode, and store it as an entry of the image's sidecar
static void _import_file(dt_iop_module_t *self)
{
  dt_iop_rasterfile_params_t *p = self->params;
  const dt_imgid_t imgid = self->dev->image_storage.id;
  if(!dt_is_valid_imgid(imgid)) return;

  gchar *mfolder = dt_conf_get_string("plugins/darkroom/segments/def_path");
  GtkFileChooserNative *filechooser = _mask_file_chooser(mfolder);
  g_free(mfolder);

  if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
  {
    gchar *filepath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
    int width = 0, height = 0, bpc = 8;
    float *mask = _read_rasterfile(filepath, p->mode, &width, &height, &bpc);
    if(mask)
    {
      dt_dtdata_ref_t ref;
      if(dt_dtdata_write_gray(imgid, DT_DTDATA_KIND_MASK, DT_DTDATA_ORIGIN_AUTHORITATIVE,
                              "rasterfile import", mask, width, height, bpc, &ref))
      {
        _params_from_ref(p, &ref);
        _update_info(self);
        dt_dev_add_history_item(darktable.develop, self, TRUE);
      }
      else
        dt_control_log(_("cannot write the raster mask sidecar for this image"));
      dt_free_align(mask);
    }
    g_free(filepath);
  }
  g_object_unref(filechooser);
}

static void _import_clicked(GtkWidget *widget, dt_iop_module_t *self)
{
  if(dt_dtdata_enabled())
    _import_file(self);
  else
    _select_folder_file(self);
}

// the button does two different things depending on the sidecar setting,
// so say which before it is clicked
static void _update_import_tooltip(dt_iop_module_t *self)
{
  dt_iop_rasterfile_gui_data_t *g = self->gui_data;
  gtk_widget_set_tooltip_text
    (g->import,
     dt_dtdata_enabled()
       ? _("import a PNG or PFM file as a raster mask\n"
           "it is stored in the image's .dtdata sidecar and travels with the image")
       : _("select a PNG or PFM file from the raster mask root folder\n"
           "sidecar files are disabled in preferences, so the mask is referenced by its path"));
}

static void _clear_cache(dt_rasterfile_cache_t *cache)
{
  dt_free_align(cache->mask);
  cache->mask = NULL;
  cache->width = cache->height = 0;
  cache->hash = DT_INVALID_HASH;
}

static inline dt_hash_t _get_cache_hash(dt_iop_module_t *self)
{
  dt_hash_t hash = dt_hash(DT_INITHASH, self->params, self->params_size);
  // not technically required but possibly reduces mem footprint
  hash = dt_hash(hash,
                 &self->dev->image_storage.id,
                 sizeof(self->dev->image_storage.id));
  return hash;
}

static float *_get_rasterfile_mask(dt_dev_pixelpipe_iop_t *piece,
                                   const dt_iop_roi_t *const roi,
                                   const dt_iop_roi_t *const roo)
{
  dt_iop_module_t *self = piece->module;
  dt_iop_rasterfile_data_t *d = piece->data;

  dt_rasterfile_cache_t *cd = self->data;
  float *res = NULL;

  dt_pthread_mutex_lock(&cd->lock);
  const dt_hash_t hash = _get_cache_hash(self);
  if(hash != cd->hash)
  {
    _clear_cache(cd);
    if(d->ref.entry[0])
    {
      dt_print(DT_DEBUG_PIPE, "read sidecar raster entry `%s'", d->ref.entry);
      cd->mask = dt_dtdata_read_gray(self->dev->image_storage.id, &d->ref,
                                     &cd->width, &cd->height);
    }
    else
    {
      dt_print(DT_DEBUG_PIPE,
               "read image raster file `%s'", d->filepath);
      cd->mask = _read_rasterfile(d->filepath, d->mode, &cd->width, &cd->height, NULL);
    }
    cd->hash = cd->mask ? hash : DT_INVALID_HASH;
    dt_print(DT_DEBUG_PIPE,
             "got raster mask data %p %dx%d", cd->mask, cd->width, cd->height);
  }
  if(cd->mask)
  {
    const gboolean scale = cd->width != roi->width || cd->height != roi->height;
    float *tmp = scale ? dt_iop_image_alloc(roi->width, roi->height, 1) : cd->mask;
    if(tmp)
    {
      if(scale)
        interpolate_bilinear(cd->mask, cd->width, cd->height, tmp,
                             roi->width, roi->height, 1);
      res = dt_iop_image_alloc(roo->width, roo->height, 1);
      if(res)
        self->distort_mask(self, piece, tmp, res, roi, roo);
      if(scale)
        dt_free_align(tmp);
    }
  }

  dt_pthread_mutex_unlock(&cd->lock);
  return res;
}

#ifdef HAVE_OPENCL
int process_cl(dt_iop_module_t *self,
               dt_dev_pixelpipe_iop_t *piece,
               cl_mem dev_in,
               cl_mem dev_out,
               const dt_iop_roi_t *const roi_in,
               const dt_iop_roi_t *const roi_out)
{
  dt_dev_pixelpipe_t *pipe = piece->pipe;
  const ssize_t ch = pipe->dsc.filters ? 1 : 4;
  const gboolean fullpipe = dt_pipe_is_full(pipe);
  const gboolean visual = fullpipe && dt_iop_has_focus(self);

  const int devid = pipe->devid;
  cl_int err = DT_OPENCL_PROCESS_CL;

  if(visual) return err;

  if(roi_out->scale != roi_in->scale && ch == 4)
    err = dt_iop_clip_and_zoom_cl(devid, dev_out, dev_in, roi_out, roi_in);
  else
  {
    const size_t iorigin[2] = { roi_out->x, roi_out->y };
    const size_t region[2] = { roi_out->width, roi_out->height };
    err = dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, iorigin, CLIMG_ORIGIN, region);
  }

  if(dt_iop_is_raster_mask_used(piece->module, BLEND_RASTER_ID)
     && (err == CL_SUCCESS))
  {
    float *mask = _get_rasterfile_mask(piece, roi_in, roi_out);
    if(mask)
      dt_iop_piece_set_raster(piece, mask, roi_in, roi_out);
    else
      dt_iop_piece_clear_raster(piece, NULL);
  }
  else
    dt_iop_piece_clear_raster(piece, NULL);

  return err;
}
#endif  // OpenCL

void init_pipe(dt_iop_module_t *self,
               dt_dev_pixelpipe_t *pipe,
               dt_dev_pixelpipe_iop_t *piece)
{
  piece->data = calloc(1, sizeof(dt_iop_rasterfile_data_t));
}

void process(dt_iop_module_t *self,
             dt_dev_pixelpipe_iop_t *piece,
             const void *const ivoid,
             void *const ovoid,
             const dt_iop_roi_t *const roi_in,
             const dt_iop_roi_t *const roi_out)
{
  const float *const in = (float *)ivoid;
  float *const out = (float *)ovoid;
  dt_dev_pixelpipe_t *pipe = piece->pipe;
  const uint32_t filters = pipe->dsc.filters;
  const ssize_t ch = filters ? 1 : 4;

  if(roi_out->scale != roi_in->scale && ch == 4)
  {
    const dt_interpolation_t *itor = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP);
    dt_interpolation_resample(itor, out, roi_out, in, roi_in);
  }
  else
    dt_iop_copy_image_roi(out, in, ch, roi_in, roi_out);

  const gboolean fullpipe = dt_pipe_is_full(pipe);
  const gboolean request = dt_iop_is_raster_mask_used(piece->module, BLEND_RASTER_ID);
  const gboolean visual = fullpipe && dt_iop_has_focus(self);
  float *mask = visual || request ? _get_rasterfile_mask(piece, roi_in, roi_out) : NULL;

  if(visual)
  {
    pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
    if(ch == 1)
    {
      // simple blur to remove CFA colors:
      dt_box_mean(out, roi_out->height, roi_out->width, 1, 3, 2);
      DT_OMP_FOR()
      for(size_t k = 0; k < (size_t)roi_out->width * roi_out->height; k++)
        out[k] =
          0.2f
          * CLAMPF(sqrtf(out[k]), 0.0f, 0.5f)
          + (mask ? mask[k] : 0.0f);
    }
    else
    {
      DT_OMP_FOR()
      for(size_t k = 0; k < (size_t)roi_out->width * roi_out->height; k++)
      {
        const float val =
          0.2f
          * CLAMPF(sqrtf(0.33f * (out[4*k] + out[4*k+1]+ out[4*k+2])), 0.0f, 0.5f)
          + (mask ? mask[k] : 0.0f);
        for_three_channels(m) out[4*k+m] = val;
      }
    }
  }

  if(request && mask)
    dt_iop_piece_set_raster(piece, mask, roi_in, roi_out);
  else
  {
    dt_iop_piece_clear_raster(piece, NULL);
    dt_free_align(mask);
  }
}

// lets the sidecar sweep see which entry a stored params blob references
static int _scan_params(const void *params,
                        const size_t size,
                        const int version,
                        char *entry,
                        const size_t len)
{
  if(version == 1) return 0; // a folder file, nothing in the sidecar
  if(version != 2 || size != sizeof(dt_iop_rasterfile_params_t)) return -1;
  const dt_iop_rasterfile_params_t *p = params;
  if(!p->entry[0]) return 0;
  g_strlcpy(entry, p->entry, len);
  return 1;
}

void init_global(dt_iop_module_so_t *self)
{
  dt_dtdata_register_scanner(self->op, 1u << DT_DTDATA_KIND_MASK, _scan_params);
}

void commit_params(dt_iop_module_t *self,
                   dt_iop_params_t *p1,
                   dt_dev_pixelpipe_t *pipe,
                   dt_dev_pixelpipe_iop_t *piece)
{
  const dt_iop_rasterfile_params_t *p = (dt_iop_rasterfile_params_t *)p1;
  dt_iop_rasterfile_data_t *d = piece->data;

  d->mode = p->mode;
  // stored params need not terminate the path or file name
  gchar *path = g_strndup(p->path, sizeof(p->path));
  gchar *file = g_strndup(p->file, sizeof(p->file));
  gchar *fullpath = g_build_filename(path, file, NULL);
  dt_strlcpy_to_fixed(d->filepath, fullpath, sizeof(d->filepath));
  g_free(fullpath);
  g_free(file);
  g_free(path);
  _ref_from_params(p, &d->ref);
}

void tiling_callback(dt_iop_module_t *self,
                     dt_dev_pixelpipe_iop_t *piece,
                     const dt_iop_roi_t *roi_in,
                     const dt_iop_roi_t *roi_out,
                     dt_develop_tiling_t *tiling)
{
  tiling->maxbuf = 1.0f;
  tiling->align = 1;
  tiling->overhead = 0;
  tiling->factor = 2.0f;
}

void reload_defaults(dt_iop_module_t *self)
{
  // we might be called from presets update infrastructure => there is
  // no image
  if(!self->dev || !dt_is_valid_imgid(self->dev->image_storage.id))
    return;

  self->default_enabled = FALSE;
  dt_iop_rasterfile_params_t *dp = self->default_params;
  memset(dp->path, 0, sizeof(dp->path));
  memset(dp->file, 0, sizeof(dp->file));
}

void distort_mask(dt_iop_module_t *self,
                  dt_dev_pixelpipe_iop_t *piece,
                  const float *const in,
                  float *const out,
                  const dt_iop_roi_t *const roi_in,
                  const dt_iop_roi_t *const roi_out)
{
  if(roi_out->scale != roi_in->scale)
  {
    const dt_interpolation_t *itor = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP);
    dt_interpolation_resample_mask(itor, out, roi_out, in, roi_in);
  }
  else
    dt_iop_copy_image_roi(out, in, 1, roi_in, roi_out);
}

void gui_changed(dt_iop_module_t *self,
                 GtkWidget *w,
                 void *previous)
{
  dt_iop_rasterfile_gui_data_t *g = self->gui_data;
  dt_iop_rasterfile_params_t *p = self->params;

  if(!w)
  {
    _update_info(self);
    _update_import_tooltip(self);
  }

  if(!w)
  {
    dt_rasterfile_cache_t *cd = self->data;
    dt_pthread_mutex_lock(&cd->lock);
    const dt_hash_t hash = _get_cache_hash(self);
    const gboolean other = hash != cd->hash;
    if(other) _clear_cache(cd);
    dt_pthread_mutex_unlock(&cd->lock);

    if(other)
      dt_dev_reprocess_center(self->dev, self->iop_order);
  }

  gtk_widget_set_sensitive(g->vectorize, _has_source(p));
}

void gui_update(dt_iop_module_t *self)
{
  dt_iop_rasterfile_params_t *p = self->params;
  // the GUI handlers read the stored path and file name as C strings
  p->path[sizeof(p->path) - 1] = '\0';
  p->file[sizeof(p->file) - 1] = '\0';
  gui_changed(self, NULL, NULL);
}

void init(dt_iop_module_t *self)
{
  dt_iop_default_init(self);

  dt_iop_rasterfile_params_t *d = self->default_params;
  memset(d->path, 0, sizeof(d->path));
  memset(d->file, 0, sizeof(d->file));

  /*
    Implementation note and reminder:

    Here we allocate per-module-instance memory shared by all pipes.
    To be sure data are valid and access is safe we

    a) ensure validity via a hash. Here it's just based on the
       parameters of the module's instance in other situations we
       might have to use the piece hash

    b) **always** access any module->data within a mutex-locked state.

    In this module the data do **not** depend on the using pipe.  In
    other cases, the pipe changing data according to a diffenrent hash
    must make sure the other pipes get restarted afterwards.
  */

  dt_rasterfile_cache_t *cd = calloc(1, sizeof(dt_rasterfile_cache_t));
  cd->hash = DT_INVALID_HASH;
  dt_pthread_mutex_init(&cd->lock);
  cd->mask = NULL;
  cd->width = cd->height = 0;
  self->data = cd;
}

void cleanup(dt_iop_module_t *self)
{
  dt_iop_default_cleanup(self);

  dt_rasterfile_cache_t *cd = self->data;
  _clear_cache(cd);
  dt_pthread_mutex_destroy(&cd->lock);
  free(cd);
  self->data = NULL;
}

void gui_focus(dt_iop_module_t *self, gboolean in)
{
  dt_dev_reprocess_center(self->dev, self->iop_order);
}

void gui_init(dt_iop_module_t *self)
{
  dt_iop_rasterfile_gui_data_t *g = IOP_GUI_ALLOC(rasterfile);

  g->mode = dt_bauhaus_combobox_from_params(self, "mode");
  gtk_widget_set_tooltip_text
    (g->mode,
     _("select the RGB channels taken into account when a color file is imported"));

  g->import = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL,
      &(dtgtk_button_config_t){
        .clicked_cb = G_CALLBACK(_import_clicked),
        .clicked_data = self,
      });
  gtk_widget_set_name(g->import, "non-flat");

  g->info = dt_ui_label_new("");
  gtk_label_set_ellipsize(GTK_LABEL(g->info), PANGO_ELLIPSIZE_MIDDLE);
  gtk_widget_set_tooltip_text
    (g->info,
     _("the raster mask this instance uses\n"
       "importing another file replaces it; masks no longer used by any\n"
       "history item are removed from the sidecar when leaving the darkroom"));

  // Vectorize button

  g->vectorize = gtk_button_new_with_label(_("vectorize"));
  gtk_widget_set_tooltip_text
    (g->vectorize,
     _("vectorize the current bitmap and create corresponding"
       " path masks in the mask manager"));
  g_signal_connect(g->vectorize, "clicked",
                   G_CALLBACK(_vectorize_button_clicked), self);

  dt_gui_box_add(self->widget,
                 dt_gui_hbox(g->import, dt_gui_expand(g->info)),
                 g->vectorize);
}

#undef RASTERFILE_MAXFILE

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on