/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * Decompose plug-in (C) 1997 Peter Kirchgessner
 * e-mail: peter@kirchgessner.net, WWW: http://www.kirchgessner.net
 *
 * Copyright 2013 Martijn van Beers <mail_dev@martijn.at>
 * Copyright 2013 Téo Mazars        <teo.mazars@ensimag.fr>
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

/*  Lab colorspace support originally written by Alexey Dyachenko,
 *  merged into the officical plug-in by Sven Neumann.
 */

#include "config.h"

#include <string.h>

#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>

#include "libgimp/stdplugins-intl.h"

#define PLUG_IN_PROC   "plug-in-decompose"
#define PLUG_IN_BINARY "decompose"
#define PLUG_IN_ROLE   "gimp-decompose"


/* Description of a component */
typedef struct
{
  const gchar    *babl_name;           /* channel's  babl_component name    */
  const gchar    *channel_name;        /* name of channel to extract        */

  const gdouble   range_min;           /* min and max                       */
  const gdouble   range_max;
  const gboolean  perceptual_channel;  /* "correct" the channel in Y' space */

} Component;


/* Maximum number of images/layers generated by an extraction */
#define MAX_EXTRACT_IMAGES 4

/* Description of an extraction */
typedef struct
{
  const gchar     *type;        /* What to extract */
  const gchar     *model;       /* the babl_model string to use */
  const gboolean   dialog;      /* Set to TRUE if you want
                                 * this extract function in the dialog */
  const gint       num_images;  /* Number of images to create */

  const gboolean   clamp;       /* clamping values in [0.0, 1.0] */

                                /* the babl_component names of the channels */
  const Component  component[MAX_EXTRACT_IMAGES];

} Extract;


typedef struct _Decompose      Decompose;
typedef struct _DecomposeClass DecomposeClass;

struct _Decompose
{
  GimpPlugIn parent_instance;
};

struct _DecomposeClass
{
  GimpPlugInClass parent_class;
};


#define DECOMPOSE_TYPE  (decompose_get_type ())
#define DECOMPOSE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), DECOMPOSE_TYPE, Decompose))

GType                   decompose_get_type         (void) G_GNUC_CONST;

static GList          * decompose_query_procedures (GimpPlugIn           *plug_in);
static GimpProcedure  * decompose_create_procedure (GimpPlugIn           *plug_in,
                                                    const gchar          *name);

static GimpValueArray * decompose_run              (GimpProcedure        *procedure,
                                                    GimpRunMode           run_mode,
                                                    GimpImage            *image,
                                                    GimpDrawable        **drawables,
                                                    GimpProcedureConfig  *config,
                                                    gpointer              run_data);

static gint        decompose                   (GimpImage           *image,
                                                GimpDrawable        *drawable,
                                                GObject             *config,
                                                GimpImage          **image_dst,
                                                gint32              *num_layers,
                                                GimpLayer          **layer_dst);
static GimpImage * create_new_image            (GFile               *file,
                                                const gchar         *layername,
                                                guint                width,
                                                guint                height,
                                                GimpImageBaseType    type,
                                                GimpPrecision        precision,
                                                gdouble              xres,
                                                gdouble              yres,
                                                GimpLayer          **layer);
static GimpLayer * create_new_layer            (GimpImage           *image,
                                                gint                 position,
                                                const gchar         *layername,
                                                guint                width,
                                                guint                height,
                                                GimpImageBaseType    type);
static void        transfer_registration_color (GeglBuffer          *src,
                                                GeglBuffer         **dst,
                                                gint                 count);
static void        cpn_affine_transform_clamp  (GeglBuffer          *buffer,
                                                gdouble              min,
                                                gdouble              max,
                                                gboolean             clamp);
static void        copy_n_components           (GeglBuffer          *src,
                                                GeglBuffer         **dst,
                                                Extract              ext);
static void        copy_one_component          (GeglBuffer          *src,
                                                GeglBuffer          *dst,
                                                const char          *model,
                                                const Component      component,
                                                gboolean             clamp);
static gboolean    decompose_dialog            (GimpProcedure       *procedure,
                                                GObject             *config);
static gchar   *   generate_filename           (GimpImage           *image,
                                                GObject             *config,
                                                guint                colorspace,
                                                guint                channel);


G_DEFINE_TYPE (Decompose, decompose, GIMP_TYPE_PLUG_IN)

GIMP_MAIN (DECOMPOSE_TYPE)
DEFINE_STD_SET_I18N


#define CPN_RGBA_R      { "R",          N_("red"),           0.0, 1.0, FALSE }
#define CPN_RGBA_G      { "G",          N_("green"),         0.0, 1.0, FALSE }
#define CPN_RGBA_B      { "B",          N_("blue"),          0.0, 1.0, FALSE }
#define CPN_RGBA_A      { "A",          N_("alpha"),         0.0, 1.0, TRUE  }

#define CPN_HSV_H       { "hue",        N_("hue"),           0.0, 1.0, TRUE }
#define CPN_HSV_S       { "saturation", N_("saturation"),    0.0, 1.0, TRUE }
#define CPN_HSV_V       { "value",      N_("value"),         0.0, 1.0, TRUE }

#define CPN_HSL_H       { "hue",        N_("hue"),           0.0, 1.0, TRUE }
#define CPN_HSL_S       { "saturation", N_("saturation"),    0.0, 1.0, TRUE }
#define CPN_HSL_L       { "lightness",  N_("lightness"),     0.0, 1.0, TRUE }

#define CPN_CMYK_C      { "Cyan",       N_("cyan"),          0.0, 1.0, TRUE }
#define CPN_CMYK_M      { "Magenta",    N_("magenta"),       0.0, 1.0, TRUE }
#define CPN_CMYK_Y      { "Yellow",     N_("yellow"),        0.0, 1.0, TRUE }
#define CPN_CMYK_K      { "Key",        N_("black"),         0.0, 1.0, TRUE }

#define CPN_LAB_L       { "CIE L",      N_("L"),             0.0, 100.0, TRUE }
#define CPN_LAB_A       { "CIE a",      N_("A"),          -127.5, 127.5, TRUE }
#define CPN_LAB_B       { "CIE b",      N_("B"),          -127.5, 127.5, TRUE }

#define CPN_LCH_L       { "CIE L",      N_("L"),             0.0, 100.0, TRUE }
#define CPN_LCH_C       { "CIE C(ab)",  N_("C"),             0.0, 200.0, TRUE }
#define CPN_LCH_H       { "CIE H(ab)",  N_("H"),             0.0, 360.0, TRUE }

#define CPN_YCBCR_Y     { "Y'",         N_("luma-y470"),       0.0, 1.0, TRUE }
#define CPN_YCBCR_CB    { "Cb",         N_("blueness-cb470"), -0.5, 0.5, TRUE }
#define CPN_YCBCR_CR    { "Cr",         N_("redness-cr470"),  -0.5, 0.5, TRUE }

#define CPN_YCBCR709_Y  { "Y'",         N_("luma-y709"),       0.0, 1.0, TRUE }
#define CPN_YCBCR709_CB { "Cb",         N_("blueness-cb709"), -0.5, 0.5, TRUE }
#define CPN_YCBCR709_CR { "Cr",         N_("redness-cr709"),  -0.5, 0.5, TRUE }


static const Extract extract[] =
{
  { N_("RGB"),   "RGB",  TRUE,  3, FALSE, { CPN_RGBA_R, CPN_RGBA_G, CPN_RGBA_B } },
  { N_("RGBA"),  "RGBA", TRUE,  4, FALSE, { CPN_RGBA_R, CPN_RGBA_G, CPN_RGBA_B, CPN_RGBA_A } },

  { N_("Red"),   "RGB",  FALSE, 1, FALSE, { CPN_RGBA_R } },
  { N_("Green"), "RGB",  FALSE, 1, FALSE, { CPN_RGBA_G } },
  { N_("Blue"),  "RGB",  FALSE, 1, FALSE, { CPN_RGBA_B } },
  { N_("Alpha"), "RGBA", TRUE , 1, FALSE, { CPN_RGBA_A } },

  { N_("HSV"),        "HSV",  TRUE,  3, FALSE, { CPN_HSV_H, CPN_HSV_S, CPN_HSV_V } },
  { N_("Hue"),        "HSV",  FALSE, 1, FALSE, { CPN_HSV_H } },
  { N_("Saturation"), "HSV",  FALSE, 1, FALSE, { CPN_HSV_S } },
  { N_("Value"),      "HSV",  FALSE, 1, FALSE, { CPN_HSV_V } },

  { N_("HSL"),              "HSL", TRUE,  3, FALSE, { CPN_HSL_H, CPN_HSL_S, CPN_HSL_L } },
  { N_("Hue (HSL)"),        "HSL", FALSE, 1, FALSE, { CPN_HSL_H } },
  { N_("Saturation (HSL)"), "HSL", FALSE, 1, FALSE, { CPN_HSL_S } },
  { N_("Lightness"),        "HSL", FALSE, 1, FALSE, { CPN_HSL_L } },

  { N_("CMYK"),      "CMYK", TRUE,  4, FALSE, { CPN_CMYK_C, CPN_CMYK_M, CPN_CMYK_Y, CPN_CMYK_K } },
  { N_("Cyan"),      "CMYK", FALSE, 1, FALSE, { CPN_CMYK_C } },
  { N_("Magenta"),   "CMYK", FALSE, 1, FALSE, { CPN_CMYK_M } },
  { N_("Yellow"),    "CMYK", FALSE, 1, FALSE, { CPN_CMYK_Y } },
  { N_("Black"),     "CMYK", FALSE, 1, FALSE, { CPN_CMYK_K } },

  { N_("LAB"), "CIE Lab",     TRUE, 3, FALSE, { CPN_LAB_L, CPN_LAB_A, CPN_LAB_B } },

  { N_("LCH"), "CIE LCH(ab)", TRUE, 3, FALSE, { CPN_LCH_L, CPN_LCH_C, CPN_LCH_H } },

  { N_("YCbCr470"),  "Y'CbCr", TRUE, 3, FALSE, { CPN_YCBCR_Y, CPN_YCBCR_CB, CPN_YCBCR_CR} },
  { N_("YCbCr470f"), "Y'CbCr", TRUE, 3, TRUE,  { CPN_YCBCR_Y, CPN_YCBCR_CB, CPN_YCBCR_CR} },

  { N_("YCbCr709"),  "Y'CbCr709", TRUE, 3, FALSE, { CPN_YCBCR709_Y, CPN_YCBCR709_CB, CPN_YCBCR709_CR} },
  { N_("YCbCr709f"), "Y'CbCr709", TRUE, 3, TRUE,  { CPN_YCBCR709_Y, CPN_YCBCR709_CB, CPN_YCBCR709_CR} }
};


static void
decompose_class_init (DecomposeClass *klass)
{
  GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);

  plug_in_class->query_procedures = decompose_query_procedures;
  plug_in_class->create_procedure = decompose_create_procedure;
  plug_in_class->set_i18n         = STD_SET_I18N;
}

static void
decompose_init (Decompose *decompose)
{
}

static GList *
decompose_query_procedures (GimpPlugIn *plug_in)
{
  return g_list_append (NULL, g_strdup (PLUG_IN_PROC));
}

static GimpProcedure *
decompose_create_procedure (GimpPlugIn  *plug_in,
                           const gchar *name)
{
  GimpProcedure *procedure = NULL;

  if (! strcmp (name, PLUG_IN_PROC))
    {
      GString *type_desc;
      gint     i;

      type_desc = g_string_new (_("What to decompose: "));
      g_string_append_c (type_desc, '"');
      g_string_append (type_desc, extract[0].type);
      g_string_append_c (type_desc, '"');

      for (i = 1; i < G_N_ELEMENTS (extract); i++)
        {
          g_string_append (type_desc, ", ");
          g_string_append_c (type_desc, '"');
          g_string_append (type_desc, extract[i].type);
          g_string_append_c (type_desc, '"');
        }

      procedure = gimp_image_procedure_new (plug_in, name,
                                            GIMP_PDB_PROC_TYPE_PLUGIN,
                                            decompose_run, NULL, NULL);

      gimp_procedure_set_image_types (procedure, "RGB*");
      gimp_procedure_set_sensitivity_mask (procedure,
                                           GIMP_PROCEDURE_SENSITIVE_DRAWABLE);

      gimp_procedure_set_menu_label (procedure, _("_Decompose..."));
      gimp_procedure_add_menu_path (procedure, "<Image>/Colors/Components");

      gimp_procedure_set_documentation (procedure,
                                        _("Decompose an image into separate "
                                          "colorspace components"),
                                        "This function creates new gray images "
                                        "with different channel information "
                                        "in each of them",
                                        name);
      gimp_procedure_set_attribution (procedure,
                                      "Peter Kirchgessner",
                                      "Peter Kirchgessner, Clarence Risher",
                                      "1997");

      gimp_procedure_add_choice_argument (procedure, "decompose-type",
                                          _("Color _model"),
                                          _("The model to decompose to"),
                                          gimp_choice_new_with_values ("rgb",        0, _("RGB"),                NULL,
                                                                       "rgba",       1, _("RGBA"),               NULL,
                                                                       "alpha",      2, _("Alpha"),              NULL,
                                                                       "hsv",        3, _("HSV"),                NULL,
                                                                       "hsl",        4, _("HSL"),                NULL,
                                                                       "cmyk",       5, _("CMYK"),               NULL,
                                                                       "lab",        6, _("LAB"),                NULL,
                                                                       "lch",        7, _("LCH"),                NULL,
                                                                       "ycbcr470",   8, _("YCbCr ITU R470"),     NULL,
                                                                       "ycbcr709",   9, _("YCbCr ITU R709"),     NULL,
                                                                       "ycbcr470f", 10, _("YCbCr ITU R470 256"), NULL,
                                                                       "ycbcr709f", 11, _("YCbCr ITU R709 256"), NULL,
                                                                       NULL),
                                          "rgb",
                                          G_PARAM_READWRITE);

      gimp_procedure_add_boolean_argument (procedure, "layers-mode",
                                           _("_Decompose to layers"),
                                           _("Create channels as layers in a single image"),
                                           TRUE,
                                           G_PARAM_READWRITE);

      gimp_procedure_add_boolean_argument (procedure, "use-registration",
                                           _("_Foreground as registration color"),
                                           _("When enabled, pixels in the foreground color "
                                             "will appear black in all output images. This "
                                             "can be used for things like crop marks that "
                                             "have to show up on all channels."),
                                           FALSE,
                                           G_PARAM_READWRITE);

      gimp_procedure_add_image_return_value (procedure, "new-image-1",
                                             "New image 1",
                                             "Output gray image 1",
                                             FALSE,
                                             G_PARAM_READWRITE);

      gimp_procedure_add_image_return_value (procedure, "new-image-2",
                                             "New image 2",
                                             "Output gray image 2 (N/A for single channel extract)",
                                             TRUE,
                                             G_PARAM_READWRITE);

      gimp_procedure_add_image_return_value (procedure, "new-image-3",
                                             "New image 3",
                                             "Output gray image 3 (N/A for single channel extract)",
                                             TRUE,
                                             G_PARAM_READWRITE);

      gimp_procedure_add_image_return_value (procedure, "new-image-4",
                                             "New image 4",
                                             "Output gray image 4 (N/A for single channel extract)",
                                             TRUE,
                                             G_PARAM_READWRITE);

      g_string_free (type_desc, TRUE);
    }

  return procedure;
}

static GimpValueArray *
decompose_run (GimpProcedure        *procedure,
               GimpRunMode           run_mode,
               GimpImage            *image,
               GimpDrawable        **drawables,
               GimpProcedureConfig  *config,
               gpointer              run_data)
{
  GimpValueArray *return_vals;
  GimpDrawable   *drawable;
  gint            num_images;
  GimpImage      *image_extract[MAX_EXTRACT_IMAGES];
  GimpLayer      *layer_extract[MAX_EXTRACT_IMAGES];
  gint            num_layers;
  GString        *data;
  gchar          *decompose_type;
  gint            j;

  gegl_init (NULL, NULL);

  if (gimp_core_object_array_get_length ((GObject **) drawables) != 1)
    {
      GError *error = NULL;

      g_set_error (&error, GIMP_PLUG_IN_ERROR, 0,
                   _("Procedure '%s' only works with one drawable."),
                   PLUG_IN_PROC);

      return gimp_procedure_new_return_values (procedure,
                                               GIMP_PDB_CALLING_ERROR,
                                               error);
    }
  else
    {
      drawable = drawables[0];
    }

  if (run_mode == GIMP_RUN_INTERACTIVE && ! decompose_dialog (procedure, G_OBJECT (config)))
    return gimp_procedure_new_return_values (procedure, GIMP_PDB_CANCEL, NULL);

  gimp_progress_init (_("Decomposing"));

  num_images = decompose (image,
                          drawable,
                          G_OBJECT (config),
                          image_extract,
                          &num_layers,
                          layer_extract);

  if (num_images <= 0)
    return gimp_procedure_new_return_values (procedure, GIMP_PDB_EXECUTION_ERROR, NULL);

  /* create decompose-data parasite */
  data = g_string_new ("");

  g_object_get (config,
                "decompose-type", &decompose_type,
                NULL);

  g_string_printf (data, "source=%d type=%s ",
                   gimp_item_get_id (GIMP_ITEM (drawable)),
                   decompose_type);

  g_free (decompose_type);

  for (j = 0; j < num_layers; j++)
    g_string_append_printf (data, "%d ",
                            gimp_item_get_id (GIMP_ITEM (layer_extract[j])));

  return_vals = gimp_procedure_new_return_values (procedure,
                                                  GIMP_PDB_SUCCESS,
                                                  NULL);

  for (j = 0; j < num_images; j++)
    {
      GimpParasite *parasite;

      GIMP_VALUES_SET_IMAGE (return_vals, j + 1, image_extract[j]);

      gimp_image_undo_enable (image_extract[j]);
      gimp_image_clean_all (image_extract[j]);

      parasite = gimp_parasite_new ("decompose-data",
                                    0, data->len + 1, data->str);
      gimp_image_attach_parasite (image_extract[j], parasite);
      gimp_parasite_free (parasite);

      if (run_mode != GIMP_RUN_NONINTERACTIVE)
        gimp_display_new (image_extract[j]);
    }

  gimp_progress_end ();

  return return_vals;
}


/* Decompose an image. It returns the number of new (gray) images.
 * The image IDs for the new images are returned in image_dst.
 * On failure, -1 is returned.
 */
static gint
decompose (GimpImage    *image,
           GimpDrawable *drawable,
           GObject      *config,
           GimpImage   **image_dst,
           gint         *nlayers,
           GimpLayer   **layer_dst)
{
  const gchar   *layername;
  gint           j, extract_idx;
  gint           height, width;
  gint           num_layers;
  GeglBuffer    *src_buffer;
  GeglBuffer    *dst_buffer[MAX_EXTRACT_IMAGES];
  GimpPrecision  precision;
  gboolean       requirements      = FALSE;
  gboolean       decomp_has_alpha = FALSE;
  gchar         *config_extract_type;
  gboolean       config_as_layers;
  gboolean       config_use_registration;

  g_object_get (config,
                "decompose-type",   &config_extract_type,
                "layers-mode",      &config_as_layers,
                "use-registration", &config_use_registration,
                NULL);

  extract_idx = -1;   /* Search extract type */
  for (j = 0; j < G_N_ELEMENTS (extract); j++)
    {
      if (g_ascii_strcasecmp (config_extract_type, extract[j].type) == 0)
        {
          extract_idx = j;
          break;
        }
    }
  if (extract_idx < 0)
    return -1;

  num_layers = extract[extract_idx].num_images;

  /* Sanity checks */
  src_buffer = gimp_drawable_get_buffer (drawable);
  precision  = gimp_image_get_precision (image);

  for (j = 0; j < num_layers; j++)
    {
      /* FIXME: Not 100% reliable */
      decomp_has_alpha |= ! g_strcmp0 ("alpha",
                                       extract[extract_idx].component[j].babl_name);
      decomp_has_alpha |= ! g_strcmp0 ("A",
                                       extract[extract_idx].component[j].babl_name);
    }

  requirements |= (gimp_drawable_is_rgb (drawable));
  requirements |= (gimp_drawable_is_indexed (drawable));
  requirements |= (gimp_drawable_is_gray (drawable)
                   && gimp_drawable_has_alpha (drawable)
                   && (num_layers <= 2)
                   && decomp_has_alpha);
  requirements &= (!decomp_has_alpha || gimp_drawable_has_alpha (drawable));

  if (!requirements)
    {
      g_message (_("Image not suitable for this decomposition"));
      return -1;
    }

  width  = gegl_buffer_get_width  (src_buffer);
  height = gegl_buffer_get_height (src_buffer);

  /* Create all new gray images */
  for (j = 0; j < num_layers; j++)
    {
      gchar   *filename;
      gdouble  xres, yres;

      filename = generate_filename (image, config, extract_idx, j);
      gimp_image_get_resolution (image, &xres, &yres);

      if (config_as_layers)
        {
          layername = gettext (extract[extract_idx].component[j].channel_name);

          if (j == 0)
            image_dst[j] = create_new_image (g_file_new_for_path (filename),
                                             layername,
                                             width, height, GIMP_GRAY, precision,
                                             xres, yres,
                                             layer_dst + j);
          else
            layer_dst[j] = create_new_layer (image_dst[0], j, layername,
                                             width, height, GIMP_GRAY);
        }
      else
        {
          image_dst[j] = create_new_image (g_file_new_for_path (filename),
                                           NULL,
                                           width, height, GIMP_GRAY, precision,
                                           xres, yres,
                                           layer_dst + j);
        }

      g_free (filename);

      dst_buffer[j] = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer_dst[j]));
    }

  copy_n_components (src_buffer, dst_buffer,
                     extract[extract_idx]);

  if (config_use_registration)
    transfer_registration_color (src_buffer, dst_buffer, num_layers);

  gimp_progress_update (1.0);

  g_object_unref (src_buffer);

  for (j = 0; j < num_layers; j++)
    {
      g_object_unref (dst_buffer[j]);
    }

  *nlayers = num_layers;

  return (config_as_layers ? 1 : num_layers);
}


/* Create an image. Returns layer and image */
static GimpImage *
create_new_image (GFile             *file,
                  const gchar       *layername,
                  guint              width,
                  guint              height,
                  GimpImageBaseType  type,
                  GimpPrecision      precision,
                  gdouble            xres,
                  gdouble            yres,
                  GimpLayer        **layer)
{
  GimpImage *image;

  image = gimp_image_new_with_precision (width, height, type, precision);

  gimp_image_undo_disable (image);
  gimp_image_set_file (image, file);
  gimp_image_set_resolution (image, xres, yres);

  *layer = create_new_layer (image, 0,
                             layername, width, height, type);

  return image;
}


static GimpLayer *
create_new_layer (GimpImage         *image,
                  gint               position,
                  const gchar       *layername,
                  guint              width,
                  guint              height,
                  GimpImageBaseType  type)
{
  GimpLayer     *layer;
  GimpImageType  gdtype = GIMP_RGB_IMAGE;

  switch (type)
    {
    case GIMP_RGB:
      gdtype = GIMP_RGB_IMAGE;
      break;
    case GIMP_GRAY:
      gdtype = GIMP_GRAY_IMAGE;
      break;
    case GIMP_INDEXED:
      gdtype = GIMP_INDEXED_IMAGE;
      break;
    }

  if (! layername)
    layername = _("Background");

  layer = gimp_layer_new (image, layername, width, height,
                          gdtype,
                          100,
                          gimp_image_get_default_new_layer_mode (image));
  gimp_image_insert_layer (image, layer, NULL, position);

  return layer;
}

/* Registration Color function */

static void
transfer_registration_color (GeglBuffer  *src,
                             GeglBuffer **dst,
                             gint         count)
{
  GeglColor          *color;
  GeglBufferIterator *gi;
  const Babl         *src_format;
  const Babl         *dst_format;
  gint                src_bpp;
  gint                dst_bpp;
  gint                i;
  gdouble             white;

  color = gimp_context_get_foreground ();
  white = 1.0;

  src_format = gegl_buffer_get_format (src);
  src_bpp = babl_format_get_bytes_per_pixel (src_format);

  dst_format = gegl_buffer_get_format (dst[0]);
  dst_bpp = babl_format_get_bytes_per_pixel (dst_format);

  gi = gegl_buffer_iterator_new (src, NULL, 0, NULL,
                                 GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 10);

  for (i = 0; i < count; i++)
    {
      gegl_buffer_iterator_add (gi, dst[i], NULL, 0, NULL,
                                GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE);
    }

  while (gegl_buffer_iterator_next (gi))
    {
      gpointer src_data;
      gpointer dst_data[MAX_EXTRACT_IMAGES];
      gint     j, k;

      src_data = gi->items[0].data;
      for (j = 0; j < count; j++)
        dst_data[j] = gi->items[j + 1].data;

      for (k = 0; k < gi->length; k++)
        {
          GeglColor *test;
          gulong     pos = k * src_bpp;

          test = gegl_color_new (NULL);
          gegl_color_set_pixel (test, src_format, ((guchar *)src_data) + pos);

          if (gimp_color_is_perceptually_identical (test, color))
            {
              for (j = 0; j < count; j++)
                {
                  gpointer data = dst_data[j];

                  babl_process (babl_fish (babl_format ("Y double"), dst_format),
                                &white, (guchar *)data + (k * dst_bpp), 1);
                }
            }

          g_object_unref (test);
        }
    }

  g_object_unref (color);
}

static void
cpn_affine_transform_clamp (GeglBuffer *buffer,
                            gdouble     min,
                            gdouble     max,
                            gboolean    clamp)
{
  GeglBufferIterator *gi;
  gdouble             scale  = 1.0 / (max - min);
  gdouble             offset = - min;

  /* We want to scale values linearly, regardless of the format of the buffer */
  gegl_buffer_set_format (buffer, babl_format ("Y double"));

  gi = gegl_buffer_iterator_new (buffer, NULL, 0, NULL,
                                 GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1);

  while (gegl_buffer_iterator_next (gi))
    {
      guint k;
      double *data;

      data = (double*) gi->items[0].data;

      if (clamp)
        {
          for (k = 0; k < gi->length; k++)
            {
              data[k] = CLAMP ((data[k] + offset) * scale, 0.0, 1.0);
            }
        }
      else
        {
          for (k = 0; k < gi->length; k++)
            {
              data[k] = (data[k] + offset) * scale;
            }
        }
    }
}

static void
copy_n_components (GeglBuffer  *src,
                   GeglBuffer **dst,
                   Extract      ext)
{
  gint i;

  for (i = 0; i < ext.num_images; i++)
    {
      gimp_progress_update ((gdouble) i / (gdouble) ext.num_images);

      copy_one_component (src, dst[i], ext.model, ext.component[i], ext.clamp);
    }
}

static void
copy_one_component (GeglBuffer      *src,
                    GeglBuffer      *dst,
                    const gchar     *model,
                    const Component  component,
                    gboolean         clamp)
{
  const Babl          *component_format;
  const Babl          *dst_format;
  GeglBuffer          *temp;
  const GeglRectangle *extent;

  /* We are working in linear double precision */
  component_format = babl_format_new (babl_model (model),
                                      babl_type ("double"),
                                      babl_component (component.babl_name),
                                      NULL);

  /* We need to enforce linearity here
   * If the output is "Y'", the output of temp is already ok
   * If the output is "Y" , it will enforce gamma-decoding.
   * A bit tricky and suboptimal...
   */
  if (component.perceptual_channel)
    dst_format = babl_format ("Y' double");
  else
    dst_format = babl_format ("Y double");

  extent = gegl_buffer_get_extent (src);
  temp = gegl_buffer_new (extent, dst_format);

  /* we want to copy the component as is */
  gegl_buffer_set_format (temp, component_format);
  gegl_buffer_copy (src, NULL, GEGL_ABYSS_NONE, temp, NULL);

  if (component.range_min != 0.0 ||
      component.range_max != 1.0 ||
      clamp)
    {
      cpn_affine_transform_clamp (temp,
                                  component.range_min, component.range_max,
                                  clamp);
    }

  /* This is our new "Y(') double" component buffer */
  gegl_buffer_set_format (temp, NULL);

  /* Now we let babl convert it back to the format that dst needs */
  gegl_buffer_copy (temp, NULL, GEGL_ABYSS_NONE, dst, NULL);

  g_object_unref (temp);
}

static gboolean
decompose_dialog (GimpProcedure *procedure,
                  GObject       *config)
{
  GtkWidget    *dialog;
  GtkWidget    *vbox;
  gboolean      run;

  gimp_ui_init (PLUG_IN_BINARY);

  dialog = gimp_procedure_dialog_new (procedure,
                                      GIMP_PROCEDURE_CONFIG (config),
                                      _("Decompose"));

  gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);

  gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog),
                                   "extract-label", _("Extract Channels"),
                                   FALSE, FALSE);
  gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog),
                                    "extract-frame",
                                    "extract-label", FALSE,
                                    "decompose-type");

  vbox = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog),
                                         "decompose-vbox",
                                         "extract-frame", "layers-mode",
                                         "use-registration",
                                         NULL);
  gtk_box_set_spacing (GTK_BOX (vbox), 12);
  gtk_container_set_border_width (GTK_CONTAINER (vbox), 12);

  gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog),
                              "decompose-vbox", NULL);

  gtk_widget_set_visible (dialog, TRUE);

  run = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog));

  gtk_widget_destroy (dialog);

  return run;
}

/* Build a filename like <imagename>-<channel>.<extension> */
gchar *
generate_filename (GimpImage *image,
                   GObject   *config,
                   guint      colorspace,
                   guint      channel)
{
  /* Build a filename like <imagename>-<channel>.<extension> */
  GFile    *file;
  gchar    *fname = NULL;
  gchar    *filename;
  gchar    *extension;
  gboolean  config_as_layers;

  g_object_get (config,
                "layers-mode", &config_as_layers,
                NULL);

  file  = gimp_image_get_file (image);
  if (file)
    fname = g_file_get_path (file);

  if (fname)
    {
      extension = fname + strlen (fname) - 1;

      while (extension >= fname)
        {
          if (*extension == '.')
            break;
          extension--;
        }

      if (extension >= fname)
        {
          *(extension++) = '\0';
        }

      if (config_as_layers)
        filename = g_strdup_printf ("%s-%s.xcf", fname,
                                    gettext (extract[colorspace].type));
      else
        filename = g_strdup_printf ("%s-%s.xcf", fname,
                                    gettext (extract[colorspace].component[channel].channel_name));
    }
  else
    {
      if (config_as_layers)
        filename = g_strdup_printf ("%s.xcf",
                                    gettext (extract[colorspace].type));
      else
        filename = g_strdup_printf ("%s.xcf",
                                    gettext (extract[colorspace].component[channel].channel_name));
    }

  g_free (fname);

  return filename;
}
