Ctrl K
ComunidadGuías…

src/iop/colorbalancergb.c

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

Archivo original ↗Preguntar a KiBot
/*
    This file is part of darktable,
    Copyright (C) 2020-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/>.
*/

// our includes go first:
#include "bauhaus/bauhaus.h"
#include "common/exif.h"
#include "common/dttypes.h"
#include "common/chromatic_adaptation.h"
#include "common/darktable_ucs_22_helpers.h"
#include "common/gamut_mapping.h"
#include "common/opencl.h"
#include "develop/blend.h"
#include "develop/imageop.h"
#include "develop/imageop_math.h"
#include "develop/openmp_maths.h"
#include "develop/imageop_gui.h"
#include "dtgtk/drawingarea.h"
#include "dtgtk/gradientslider.h"
#include "gui/accelerators.h"
#include "gui/draw.h"
#include "gui/gtk.h"
#include "gui/presets.h"
#include "gui/color_picker_proxy.h"
#include "iop/iop_api.h"

//#include <gtk/gtk.h>
#include <stdlib.h>
#define STEPS 92         // so we test 92×92×92 combinations of RGB in [0; 1] to build the gamut LUT

// Filmlight Yrg puts red at 330°, while usual HSL wheels put it at 360/0°
// so shift in GUI only it to not confuse people. User params are always degrees,
// pixel params are always radians.
#define ANGLE_SHIFT -30.f
#define CONVENTIONAL_DEG_TO_YRG_RAD(x) (deg2radf(x + ANGLE_SHIFT))
#define YRG_RAD_TO_CONVENTIONAL_DEG(x) (rad2degf(x) - ANGLE_SHIFT)

DT_MODULE_INTROSPECTION(5, dt_iop_colorbalancergb_params_t)

typedef enum dt_iop_colorbalancrgb_saturation_t
{
  DT_COLORBALANCE_SATURATION_JZAZBZ = 0, // $DESCRIPTION: "JzAzBz (2021)"
  DT_COLORBALANCE_SATURATION_DTUCS = 1   // $DESCRIPTION: "darktable UCS (2022)"
} dt_iop_colorbalancrgb_saturation_t;

typedef struct dt_iop_colorbalancergb_params_t
{
  /* params of v1 */
  float shadows_Y;             // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "luminance"
  float shadows_C;             // $MIN:  0.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "chroma"
  float shadows_H;             // $MIN:  0.0 $MAX: 360.0 $DEFAULT: 0.0 $DESCRIPTION: "hue"
  float midtones_Y;            // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "luminance"
  float midtones_C;            // $MIN:  0.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "chroma"
  float midtones_H;            // $MIN:  0.0 $MAX: 360.0 $DEFAULT: 0.0 $DESCRIPTION: "hue"
  float highlights_Y;          // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "luminance"
  float highlights_C;          // $MIN:  0.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "chroma"
  float highlights_H;          // $MIN:  0.0 $MAX: 360.0 $DEFAULT: 0.0 $DESCRIPTION: "hue"
  float global_Y;              // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "luminance"
  float global_C;              // $MIN:  0.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "chroma"
  float global_H;              // $MIN:  0.0 $MAX: 360.0 $DEFAULT: 0.0 $DESCRIPTION: "hue"
  float shadows_weight;        // $MIN:  0.0 $MAX:   3.0 $DEFAULT: 1.0 $DESCRIPTION: "shadows fall-off"
  float white_fulcrum;         // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "white fulcrum"
  float highlights_weight;     // $MIN:  0.0 $MAX:   3.0 $DEFAULT: 1.0 $DESCRIPTION: "highlights fall-off"
  float chroma_shadows;        // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
  float chroma_highlights;     // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
  float chroma_global;         // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "global chroma"
  float chroma_midtones;       // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
  float saturation_global;     // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "global saturation"
  float saturation_highlights; // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
  float saturation_midtones;   // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
  float saturation_shadows;    // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
  float hue_angle;             // $MIN: -180. $MAX: 180. $DEFAULT: 0.0 $DESCRIPTION: "hue shift"

  /* params of v2 */
  float brilliance_global;     // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "global brilliance"
  float brilliance_highlights; // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
  float brilliance_midtones;   // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
  float brilliance_shadows;    // $MIN: -1.0 $MAX:   1.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"

  /* params of v3 */
  float mask_grey_fulcrum;     // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.1845 $DESCRIPTION: "mask middle-gray fulcrum"

  /* params of v4 */
  float vibrance;         // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0 $DESCRIPTION: "global vibrance"
  float grey_fulcrum;     // $MIN:  0.0 $MAX: 1.0 $DEFAULT: 0.1845 $DESCRIPTION: "contrast gray fulcrum"
  float contrast;         // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0. $DESCRIPTION: "contrast"

  /* params of v5 */
  dt_iop_colorbalancrgb_saturation_t saturation_formula; // $DEFAULT: 1 $DESCRIPTION: "saturation formula"

  /* add future params after this so the legacy params import can use a blind memcpy */
} dt_iop_colorbalancergb_params_t;


typedef enum dt_iop_colorbalancergb_mask_data_t
{
  MASK_SHADOWS = 0,
  MASK_MIDTONES = 1,
  MASK_HIGHLIGHTS = 2,
  MASK_NONE
} dt_iop_colorbalancergb_mask_data_t;


typedef struct dt_iop_colorbalancergb_gui_data_t
{
  GtkWidget *shadows_H, *midtones_H, *highlights_H, *global_H;
  GtkWidget *shadows_C, *midtones_C, *highlights_C, *global_C;
  GtkWidget *shadows_Y, *midtones_Y, *highlights_Y, *global_Y;
  GtkWidget *shadows_weight, *mask_grey_fulcrum, *highlights_weight;
  GtkWidget *chroma_highlights, *chroma_global, *chroma_shadows, *chroma_midtones, *vibrance;
  GtkWidget *contrast, *grey_fulcrum, *white_fulcrum;
  GtkWidget *saturation_global, *saturation_highlights, *saturation_midtones, *saturation_shadows;
  GtkWidget *brilliance_global, *brilliance_highlights, *brilliance_midtones, *brilliance_shadows;
  GtkWidget *saturation_formula;
  GtkWidget *hue_angle;
  GtkDrawingArea *area;
  GtkNotebook *notebook;
  GtkWidget *checker_color_1_picker, *checker_color_2_picker, *checker_size;
  gboolean mask_display;
  dt_iop_colorbalancergb_mask_data_t mask_type;
  const dt_iop_order_iccprofile_info_t *sliders_output_profile;
} dt_iop_colorbalancergb_gui_data_t;


typedef struct dt_iop_colorbalancergb_data_t
{
  float global[4];
  float shadows[4];
  float highlights[4];
  float midtones[4];
  float midtones_Y;
  float chroma_global, chroma[4], vibrance, contrast;
  float saturation_global, saturation[4];
  float brilliance_global, brilliance[4];
  float hue_angle;
  float shadows_weight, highlights_weight, midtones_weight, mask_grey_fulcrum;
  float white_fulcrum, grey_fulcrum;
  float *gamut_LUT;
  float max_chroma;
  dt_aligned_pixel_t checker_color_1, checker_color_2;
  dt_iop_colorbalancrgb_saturation_t saturation_formula;
  size_t checker_size;
  gboolean lut_inited;
  struct dt_iop_order_iccprofile_info_t *work_profile;
} dt_iop_colorbalancergb_data_t;

typedef struct dt_iop_colorbalance_global_data_t
{
  int kernel_colorbalance_rgb;
} dt_iop_colorbalancergb_global_data_t;


const char *name()
{
  return _("color balance rgb");
}

const char *aliases()
{
  return _("offset power slope|cdl|color grading|contrast|chroma_highlights|hue|vibrance|saturation");
}

const char **description(dt_iop_module_t *self)
{
  return dt_iop_set_description(self, _("color grading tools using alpha masks to separate\n"
                                        "shadows, mid-tones and highlights"),
                                      _("corrective or creative"),
                                      _("linear, RGB, scene-referred"),
                                      _("non-linear, RGB"),
                                      _("non-linear, RGB, scene-referred"));
}

int flags()
{
  return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING | IOP_FLAGS_ALLOW_TILING;
}

int default_group()
{
  return IOP_GROUP_COLOR | IOP_GROUP_GRADING;
}

dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,
                                            dt_dev_pixelpipe_t *pipe,
                                            dt_dev_pixelpipe_iop_t *piece)
{
  return IOP_CS_RGB;
}

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_colorbalancergb_params_v5_t
  {
    /* params of v1 */
    float shadows_Y;
    float shadows_C;
    float shadows_H;
    float midtones_Y;
    float midtones_C;
    float midtones_H;
    float highlights_Y;
    float highlights_C;
    float highlights_H;
    float global_Y;
    float global_C;
    float global_H;
    float shadows_weight;
    float white_fulcrum;
    float highlights_weight;
    float chroma_shadows;
    float chroma_highlights;
    float chroma_global;
    float chroma_midtones;
    float saturation_global;
    float saturation_highlights;
    float saturation_midtones;
    float saturation_shadows;
    float hue_angle;

    /* params of v2 */
    float brilliance_global;
    float brilliance_highlights;
    float brilliance_midtones;
    float brilliance_shadows;

    /* params of v3 */
    float mask_grey_fulcrum;

    /* params of v4 */
    float vibrance;
    float grey_fulcrum;
    float contrast;

    /* params of v5 */
    dt_iop_colorbalancrgb_saturation_t saturation_formula;

    /* add future params after this so the legacy params import can use a blind memcpy */
  } dt_iop_colorbalancergb_params_v5_t;

  const dt_iop_colorbalancergb_params_v5_t default_v5 =
    { 0.0f,  0.0f,  0.0f,  0.0f,    0.0f,
      0.0f,  0.0f,  0.0f,  0.0f,    0.0f,
      0.0f,  0.0f,  1.0f,  0.0f,    1.0f,
      0.0f,  0.0f,  0.0f,  0.0f,    0.0f,
      0.0f,  0.0f,  0.0f,  0.0f,    0.0f,
      0.0f,  0.0f,  0.0f,  0.1845f, 0.0f,
      0.0f,  0.0f,  1
    };

  if(old_version == 1)
  {
    typedef struct dt_iop_colorbalancergb_params_v1_t
    {
      float shadows_Y;
      float shadows_C;
      float shadows_H;
      float midtones_Y;
      float midtones_C;
      float midtones_H;
      float highlights_Y;
      float highlights_C;
      float highlights_H;
      float global_Y;
      float global_C;
      float global_H;
      float shadows_weight;
      float white_fulcrum;
      float highlights_weight;
      float chroma_shadows;
      float chroma_highlights;
      float chroma_global;
      float chroma_midtones;
      float saturation_global;
      float saturation_highlights;
      float saturation_midtones;
      float saturation_shadows;
      float hue_angle;
    } dt_iop_colorbalancergb_params_v1_t;

    const dt_iop_colorbalancergb_params_v1_t *o = old_params;
    dt_iop_colorbalancergb_params_v5_t *n =
      malloc(sizeof(dt_iop_colorbalancergb_params_v5_t));

    // Init params with defaults
    memcpy(n, &default_v5, sizeof(dt_iop_colorbalancergb_params_v5_t));

    // Copy the common part of the params struct
    memcpy(n, o, sizeof(dt_iop_colorbalancergb_params_v1_t));

    n->saturation_global /= 100.f;
    n->mask_grey_fulcrum = 0.1845f;
    n->vibrance = 0.f;
    n->grey_fulcrum = 0.1845f;
    n->contrast = 0.f;
    n->saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;

    *new_params = n;
    *new_params_size = sizeof(dt_iop_colorbalancergb_params_v5_t);
    *new_version = 5;
    return 0;
  }

  if(old_version == 2)
  {
    typedef struct dt_iop_colorbalancergb_params_v2_t
    {
      /* params of v1 */
      float shadows_Y;
      float shadows_C;
      float shadows_H;
      float midtones_Y;
      float midtones_C;
      float midtones_H;
      float highlights_Y;
      float highlights_C;
      float highlights_H;
      float global_Y;
      float global_C;
      float global_H;
      float shadows_weight;
      float white_fulcrum;
      float highlights_weight;
      float chroma_shadows;
      float chroma_highlights;
      float chroma_global;
      float chroma_midtones;
      float saturation_global;
      float saturation_highlights;
      float saturation_midtones;
      float saturation_shadows;
      float hue_angle;

      /* params of v2 */
      float brilliance_global;
      float brilliance_highlights;
      float brilliance_midtones;
      float brilliance_shadows;
    } dt_iop_colorbalancergb_params_v2_t;

    const dt_iop_colorbalancergb_params_v2_t *o = old_params;
    dt_iop_colorbalancergb_params_v5_t *n =
      malloc(sizeof(dt_iop_colorbalancergb_params_v5_t));

    // Init params with defaults
    memcpy(n, &default_v5, sizeof(dt_iop_colorbalancergb_params_v5_t));

    // Copy the common part of the params struct
    memcpy(n, o, sizeof(dt_iop_colorbalancergb_params_v2_t));

    n->mask_grey_fulcrum = 0.1845f;
    n->vibrance = 0.f;
    n->grey_fulcrum = 0.1845f;
    n->contrast = 0.f;
    n->saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;

    *new_params = n;
    *new_params_size = sizeof(dt_iop_colorbalancergb_params_v5_t);
    *new_version = 5;
    return 0;
  }
  if(old_version == 3)
  {
    typedef struct dt_iop_colorbalancergb_params_v3_t
    {
      /* params of v1 */
      float shadows_Y;
      float shadows_C;
      float shadows_H;
      float midtones_Y;
      float midtones_C;
      float midtones_H;
      float highlights_Y;
      float highlights_C;
      float highlights_H;
      float global_Y;
      float global_C;
      float global_H;
      float shadows_weight;
      float white_fulcrum;
      float highlights_weight;
      float chroma_shadows;
      float chroma_highlights;
      float chroma_global;
      float chroma_midtones;
      float saturation_global;
      float saturation_highlights;
      float saturation_midtones;
      float saturation_shadows;
      float hue_angle;

      /* params of v2 */
      float brilliance_global;
      float brilliance_highlights;
      float brilliance_midtones;
      float brilliance_shadows;

      /* params of v3 */
      float mask_grey_fulcrum;
    } dt_iop_colorbalancergb_params_v3_t;

    const dt_iop_colorbalancergb_params_v3_t *o = old_params;
    dt_iop_colorbalancergb_params_v5_t *n =
      malloc(sizeof(dt_iop_colorbalancergb_params_v5_t));

    // Init params with defaults
    memcpy(n, &default_v5, sizeof(dt_iop_colorbalancergb_params_v5_t));

    // Copy the common part of the params struct
    memcpy(n, o, sizeof(dt_iop_colorbalancergb_params_v3_t));

    n->vibrance = 0.f;
    n->grey_fulcrum = 0.1845f;
    n->contrast = 0.f;
    n->saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;

    *new_params = n;
    *new_params_size = sizeof(dt_iop_colorbalancergb_params_v5_t);
    *new_version = 5;
    return 0;
  }
  if(old_version == 4)
  {
    typedef struct dt_iop_colorbalancergb_params_v4_t
    {
      /* params of v1 */
      float shadows_Y;
      float shadows_C;
      float shadows_H;
      float midtones_Y;
      float midtones_C;
      float midtones_H;
      float highlights_Y;
      float highlights_C;
      float highlights_H;
      float global_Y;
      float global_C;
      float global_H;
      float shadows_weight;
      float white_fulcrum;
      float highlights_weight;
      float chroma_shadows;
      float chroma_highlights;
      float chroma_global;
      float chroma_midtones;
      float saturation_global;
      float saturation_highlights;
      float saturation_midtones;
      float saturation_shadows;
      float hue_angle;

      /* params of v2 */
      float brilliance_global;
      float brilliance_highlights;
      float brilliance_midtones;
      float brilliance_shadows;

      /* params of v3 */
      float mask_grey_fulcrum;

      /* params of v4 */
      float vibrance;
      float grey_fulcrum;
      float contrast;
    } dt_iop_colorbalancergb_params_v4_t;

    const dt_iop_colorbalancergb_params_v4_t *o = old_params;
    dt_iop_colorbalancergb_params_v5_t *n =
      malloc(sizeof(dt_iop_colorbalancergb_params_v5_t));

    // Init params with defaults
    memcpy(n, &default_v5, sizeof(dt_iop_colorbalancergb_params_v5_t));

    // Copy the common part of the params struct
    memcpy(n, o, sizeof(dt_iop_colorbalancergb_params_v4_t));

    n->saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;

    *new_params = n;
    *new_params_size = sizeof(dt_iop_colorbalancergb_params_v5_t);
    *new_version = 5;
    return 0;
  }

  return 1;
}

void init_presets(dt_iop_module_so_t *self)
{
  // Note : all the elements of the params structure are scalar floats,
  // so we can just init them all to 0.f in batch
  // Then, only 4 params have to be manually inited to non-zero values
  dt_iop_colorbalancergb_params_t p = { 0.f };
  p.shadows_weight = 1.f;        // DEFAULT: 1.0 DESCRIPTION: "shadows fall-off"
  p.highlights_weight = 1.f;     // DEFAULT: 1.0 DESCRIPTION: "highlights fall-off"
  p.mask_grey_fulcrum = 0.1845f; // DEFAULT: 0.1845 DESCRIPTION: "mask middle-gray fulcrum"
  p.grey_fulcrum = 0.1845f;      // DEFAULT: 0.1845 DESCRIPTION: "contrast gray fulcrum"
  p.saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;

  // preset
  p.chroma_global = 0.2f;
  p.saturation_shadows = 0.1f;
  p.saturation_midtones = 0.05f;
  p.saturation_highlights = -0.05f;

  dt_gui_presets_add_generic(_("basic colorfulness | legacy"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);

  p.saturation_formula = DT_COLORBALANCE_SATURATION_DTUCS;
  p.chroma_global = 0.f;

  p.saturation_global = 0.2f;
  p.saturation_shadows = 0.30f;
  p.saturation_midtones = 0.f;
  p.saturation_highlights = -0.5f;
  dt_gui_presets_add_generic(_("basic colorfulness | natural skin"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);

  p.saturation_global = 0.2f;
  p.saturation_shadows = 0.5f;
  p.saturation_midtones = 0.f;
  p.saturation_highlights = -0.25f;
  dt_gui_presets_add_generic(_("basic colorfulness | vibrant colors"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);

  p.saturation_global = 0.2f;
  p.saturation_shadows = 0.25f;
  p.saturation_midtones = 0.f;
  p.saturation_highlights = -0.25f;
  dt_gui_presets_add_generic(_("basic colorfulness | standard"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
}



DT_OMP_DECLARE_SIMD(aligned(output, output_comp: 16) uniform(shadows_weight, midtones_weight, highlights_weight))
static inline void opacity_masks(const float x,
                                 const float shadows_weight, const float highlights_weight,
                                 const float midtones_weight, const float mask_grey_fulcrum,
                                 dt_aligned_pixel_t output, dt_aligned_pixel_t output_comp)
{
  const float x_offset = (x - mask_grey_fulcrum);
  const float x_offset_norm = x_offset / mask_grey_fulcrum;
  const float alpha = 1.f / (1.f + expf(x_offset_norm * shadows_weight));    // opacity of shadows
  const float beta = 1.f / (1.f + expf(-x_offset_norm * highlights_weight)); // opacity of highlights
  const float alpha_comp = 1.f - alpha;
  const float beta_comp = 1.f - beta;
  const float gamma = expf(-sqf(x_offset) * midtones_weight / 4.f) * sqf(alpha_comp) * sqf(beta_comp) * 8.f; // opacity of midtones
  const float gamma_comp = 1.f - gamma;

  output[0] = alpha;
  output[1] = gamma;
  output[2] = beta;
  output[3] = 0.f;

  if(output_comp)
  {
    output_comp[0] = alpha_comp;
    output_comp[1] = gamma_comp;
    output_comp[2] = beta_comp;
    output_comp[3] = 0.f;
  }
}

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)
{
  dt_iop_colorbalancergb_data_t *d = piece->data;
  dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
  const dt_iop_order_iccprofile_info_t *const work_profile
      = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
  if(work_profile == NULL) return; // no point


  // work profile can't be fetched in commit_params since it is not yet initialised
  // work_profile->matrix_in === RGB_to_XYZ
  // work_profile->matrix_out === XYZ_to_RGB

  // Premultiply the input matrices

  /* What we do here is equivalent to :

    // go to CIE 1931 XYZ 2° D50
    dot_product(RGB, RGB_to_XYZ, XYZ_D50); // matrice product

    // chroma adapt D50 to D65
    XYZ_D50_to_65(XYZ_D50, XYZ_D65);       // matrice product

    // go to CIE 2006 LMS
    XYZ_to_LMS(XYZ_D65, LMS);              // matrice product

  * so we pre-multiply the 3 conversion matrices and operate only one matrix product
  */
  dt_colormatrix_t input_matrix = { { 0.0f } };
  dt_colormatrix_t output_matrix = { { 0.0f } };

  dt_colormatrix_mul(output_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in); // output_matrix used as temp buffer
  dt_colormatrix_mul(input_matrix, XYZ_D65_to_LMS_2006_D65, output_matrix);
  dt_colormatrix_t input_matrix_trans;
  dt_colormatrix_transpose(input_matrix_trans, input_matrix);

  // Premultiply the output matrix

  /* What we do here is equivalent to :
    XYZ_D65_to_50(XYZ_D65, XYZ_D50);           // matrix product
    dot_product(XYZ_D50, XYZ_to_RGB, pix_out); // matrix product
  */

  dt_colormatrix_mul(output_matrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
  dt_colormatrix_t output_matrix_trans;
  dt_colormatrix_transpose(output_matrix_trans, output_matrix);

  const float *const restrict in = DT_IS_ALIGNED(((const float *const restrict)ivoid));
  float *const restrict out = DT_IS_ALIGNED(((float *const restrict)ovoid));
  const float *const restrict gamut_LUT = DT_IS_ALIGNED(((const float *const restrict)d->gamut_LUT));

  const float *const restrict global = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->global);
  const float *const restrict highlights = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->highlights);
  const float *const restrict shadows = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->shadows);
  const float *const restrict midtones = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->midtones);

  const float *const restrict chroma = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->chroma);
  const float *const restrict saturation = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->saturation);
  const float *const restrict brilliance = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->brilliance);

  gboolean mask_display = FALSE;
  dt_iop_colorbalancergb_mask_data_t mask_type = MASK_NONE;
  if(self->dev->gui_attached && g && dt_pipe_is_full(piece->pipe))
  {
    dt_iop_gui_enter_critical_section(self);
    mask_display = g->mask_display;
    mask_type = g->mask_type;
    dt_iop_gui_leave_critical_section(self);
  }

  // pixel size of the checker background
  const size_t checker_1 = mask_display ? DT_PIXEL_APPLY_DPI(d->checker_size) : 0;
  const size_t checker_2 = 2 * checker_1;

  const float L_white = Y_to_dt_UCS_L_star(d->white_fulcrum);

  const float DT_ALIGNED_ARRAY hue_rotation_matrix[2][2] = {
    { cosf(d->hue_angle), -sinf(d->hue_angle) },
    { sinf(d->hue_angle),  cosf(d->hue_angle) },
  };

  const size_t npixels = (size_t)roi_out->height * roi_out->width;
  const size_t out_width = roi_out->width;

  DT_OMP_FOR()
  for(size_t k  = 0; k < 4 * npixels; k += 4)
  {
    // clip pipeline RGB
    dt_aligned_pixel_t RGB;
    copy_pixel(RGB, in + k);
    dt_vector_clipneg(RGB);

    // go to CIE 2006 LMS D65
    dt_aligned_pixel_t LMS;
    dt_apply_transposed_color_matrix(RGB, input_matrix_trans, LMS);

    /* The previous line is equivalent to :
      // go to CIE 1931 XYZ 2° D50
      dot_product(RGB, RGB_to_XYZ, XYZ_D50); // matrice product

      // chroma adapt D50 to D65
      XYZ_D50_to_65(XYZ_D50, XYZ_D65); // matrice product

      // go to CIE 2006 LMS
      XYZ_to_LMS(XYZ_D65, LMS); // matrice product
    */

    // go to Filmlight Yrg
    dt_aligned_pixel_t Yrg = { 0.f };
    LMS_to_Yrg(LMS, Yrg);

    // go to Ych
    dt_aligned_pixel_t Ych = { 0.f };
    Yrg_to_Ych(Yrg, Ych);

    // Sanitize input : no negative luminance
    Ych[0] = MAX(Ych[0], 0.f);

    // Opacities for luma masks
    dt_aligned_pixel_t opacities;
    dt_aligned_pixel_t opacities_comp;
    opacity_masks(powf(Ych[0], 0.4101205819200422f), // center middle grey in 50 %
                  d->shadows_weight, d->highlights_weight, d->midtones_weight,
                  d->mask_grey_fulcrum, opacities, opacities_comp);

    // Hue shift - do it now because we need the gamut limit at output hue right after
    // The hue rotation is implemented as a matrix multiplication.
    const float cos_h = Ych[2];
    const float sin_h = Ych[3];
    Ych[2] = hue_rotation_matrix[0][0] * cos_h + hue_rotation_matrix[0][1] * sin_h;
    Ych[3] = hue_rotation_matrix[1][0] * cos_h + hue_rotation_matrix[1][1] * sin_h;

    // Linear chroma : distance to achromatic at constant luminance in scene-referred
    const float chroma_boost = d->chroma_global + scalar_product(opacities, chroma);
    const float vibrance = d->vibrance * (1.0f - powf(Ych[1], fabsf(d->vibrance)));
    const float chroma_factor = MAX(1.f + chroma_boost + vibrance, 0.f);
    Ych[1] *= chroma_factor;

    // clip chroma at constant hue and Y if needed
    gamut_check_Yrg(Ych);

    // go to Yrg for real
    Ych_to_Yrg(Ych, Yrg);

    // Go to LMS
    Yrg_to_LMS(Yrg, LMS);

    // Go to Filmlight RGB
    LMS_to_gradingRGB(LMS, RGB);

    // Color balance
    for_four_channels(c, aligned(RGB, global))
    {
      // global : offset
      RGB[c] += global[c];
    }
    for_four_channels(c, aligned(RGB, opacities, opacities_comp, shadows, midtones, highlights:16))
    {
      //  highlights, shadows : 2 slopes with masking
      RGB[c] *= opacities_comp[2] * (opacities_comp[0] + opacities[0] * shadows[c]) + opacities[2] * highlights[c];
      // factorization of : (RGB[c] * (1.f - alpha) + RGB[c] * d->shadows[c] * alpha) * (1.f - beta)  + RGB[c] * d->highlights[c] * beta;
    }
    dt_aligned_pixel_t sign;
    for_each_channel(c)
      sign[c] = (RGB[c] < 0.f) ? -1.f : 1.f;
    dt_aligned_pixel_t abs_RGB;
    for_each_channel(c)
      abs_RGB[c] = fabsf(RGB[c]);
    dt_aligned_pixel_t scaled_RGB;
    for_each_channel(c)
      scaled_RGB[c] = abs_RGB[c] /d->white_fulcrum;
    dt_vector_powf(scaled_RGB, midtones, RGB);
    for_each_channel(c)
      RGB[c] = RGB[c] * sign[c] * d->white_fulcrum;

    // for the non-linear ops we need to go in Yrg again because RGB doesn't preserve color
    gradingRGB_to_LMS(RGB, LMS);
    LMS_to_Yrg(LMS, Yrg);

    // Y midtones power (gamma)
    Yrg[0] = powf(MAX(Yrg[0] / d->white_fulcrum, 0.f), d->midtones_Y) * d->white_fulcrum;

    // Y fulcrumed contrast
    Yrg[0] = d->grey_fulcrum * powf(Yrg[0] / d->grey_fulcrum, d->contrast);

    Yrg_to_LMS(Yrg, LMS);
    dt_aligned_pixel_t XYZ_D65 = { 0.f };
    LMS_to_XYZ(LMS, XYZ_D65);

    // Perceptual color adjustments
    if(d->saturation_formula == DT_COLORBALANCE_SATURATION_JZAZBZ)
    {
      dt_aligned_pixel_t Jab = { 0.f };
      dt_XYZ_2_JzAzBz(XYZ_D65, Jab);

      // Convert to JCh
      float JC[2] = { Jab[0], dt_fast_hypotf(Jab[1], Jab[2]) };   // brightness/chroma vector
      const float h = atan2f(Jab[2], Jab[1]);  // hue : (a, b) angle

      // Project JC onto S, the saturation eigenvector, with orthogonal vector O.
      // Note : O should be = (C * cosf(T) - J * sinf(T)) = 0 since S is the eigenvector,
      // so we add the chroma projected along the orthogonal axis to get some control value
      const float T = atan2f(JC[1], JC[0]); // angle of the eigenvector over the hue plane
      const float sin_T = sinf(T);
      const float cos_T = cosf(T);
      const float DT_ALIGNED_PIXEL M_rot_dir[2][2] = { {  cos_T,  sin_T },
                                                      { -sin_T,  cos_T } };
      const float DT_ALIGNED_PIXEL M_rot_inv[2][2] = { {  cos_T, -sin_T },
                                                      {  sin_T,  cos_T } };
      float SO[2];

      // brilliance & Saturation : mix of chroma and luminance
      const float boosts[2] = { 1.f + d->brilliance_global + scalar_product(opacities, brilliance),     // move in S direction
                                d->saturation_global + scalar_product(opacities, saturation) }; // move in O direction

      SO[0] = JC[0] * M_rot_dir[0][0] + JC[1] * M_rot_dir[0][1];
      SO[1] = SO[0] * MIN(MAX(T * boosts[1], -T), M_PI_2f - T);
      SO[0] = MAX(SO[0] * boosts[0], 0.f);

      // Project back to JCh, that is rotate back of -T angle
      JC[0] = MAX(SO[0] * M_rot_inv[0][0] + SO[1] * M_rot_inv[0][1], 0.f);
      JC[1] = MAX(SO[0] * M_rot_inv[1][0] + SO[1] * M_rot_inv[1][1], 0.f);

      // Gamut mapping
      const float out_max_sat_h = lookup_gamut(gamut_LUT, h);
      // if JC[0] == 0.f, the saturation / luminance ratio is infinite - assign the largest practical value we have
      const float sat = (JC[0] > 0.f) ? soft_clip(JC[1] / JC[0], 0.8f * out_max_sat_h, out_max_sat_h)
                                      : out_max_sat_h;
      const float max_C_at_sat = JC[0] * sat;
      // if sat == 0.f, the chroma is zero - assign the original luminance because there's no need to gamut map
      const float max_J_at_sat = (sat > 0.f) ? JC[1] / sat : JC[0];
      JC[0] = (JC[0] + max_J_at_sat) / 2.f;
      JC[1] = (JC[1] + max_C_at_sat) / 2.f;

      // Gamut-clip in Jch at constant hue and lightness,
      // e.g. find the max chroma available at current hue that doesn't
      // yield negative L'M'S' values, which will need to be clipped during conversion
      const float cos_H = cosf(h);
      const float sin_H = sinf(h);

      const float d0 = 1.6295499532821566e-11f;
      const float dd = -0.56f;
      float Iz = JC[0] + d0;
      Iz /= (1.f + dd - dd * Iz);
      Iz = MAX(Iz, 0.f);

      static const dt_colormatrix_t AI_trans
          = { {  1.0f,                 1.0f,                                1.0f, 0.0f },
              {  0.1386050432715393f, -0.1386050432715393f, -0.0960192420263190f, 0.0f },
              {  0.0580473161561189f, -0.0580473161561189f, -0.8118918960560390f, 0.0f } };

      // Do a test conversion to L'M'S'
      const dt_aligned_pixel_t IzAzBz = { Iz, JC[1] * cos_H, JC[1] * sin_H, 0.f };
      dt_apply_transposed_color_matrix(IzAzBz, AI_trans, LMS);

      // Clip chroma
      float max_C = JC[1];
      if(LMS[0] < 0.f)
        max_C = MIN(-Iz / (AI_trans[1][0] * cos_H + AI_trans[2][0] * sin_H), max_C);

      if(LMS[1] < 0.f)
        max_C = MIN(-Iz / (AI_trans[1][1] * cos_H + AI_trans[2][1] * sin_H), max_C);

      if(LMS[2] < 0.f)
        max_C = MIN(-Iz / (AI_trans[1][2] * cos_H + AI_trans[2][2] * sin_H), max_C);

      // Project back to JzAzBz for real
      Jab[0] = JC[0];
      Jab[1] = max_C * cos_H;
      Jab[2] = max_C * sin_H;

      dt_JzAzBz_2_XYZ(Jab, XYZ_D65);
    }
    else
    {
      dt_aligned_pixel_t xyY, JCH, HCB;
      dt_D65_XYZ_to_xyY(XYZ_D65, xyY);
      xyY_to_dt_UCS_JCH(xyY, L_white, JCH);
      dt_UCS_JCH_to_HCB(JCH, HCB);

      const float radius = dt_fast_hypotf(HCB[1], HCB[2]);
      const float sin_T = (radius > 0.f) ? HCB[1] / radius : 0.f;
      const float cos_T = (radius > 0.f) ? HCB[2] / radius : 0.f;
      const float DT_ALIGNED_PIXEL M_rot_inv[2][2] = { { cos_T,  sin_T }, { -sin_T, cos_T } };
      // This would be the full matrice of direct rotation if we didn't need only its last row
      //const float DT_ALIGNED_PIXEL M_rot_dir[2][2] = { { cos_T, -sin_T }, {  sin_T, cos_T } };

      const float P = MAX(FLT_MIN, HCB[1]); // as HCB[1] is at least zero we don't fiddle with sign
      const float W = sin_T * HCB[1] + cos_T * HCB[2];

      float a = MAX(1.f + d->saturation_global + scalar_product(opacities, saturation), 0.f);
      const float b = MAX(1.f + d->brilliance_global + scalar_product(opacities, brilliance), 0.f);

      const float max_a = dt_fast_hypotf(P, W) / P;
      a = soft_clip(a, 0.5f * max_a, max_a);

      const float P_prime = (a - 1.f) * P;
      const float W_prime = sqrtf(sqf(P) * (1.f - sqf(a)) + sqf(W)) * b;

      HCB[1] = MAX(M_rot_inv[0][0] * P_prime + M_rot_inv[0][1] * W_prime, 0.f);
      HCB[2] = MAX(M_rot_inv[1][0] * P_prime + M_rot_inv[1][1] * W_prime, 0.f);

      dt_UCS_HCB_to_JCH(HCB, JCH);

      // Gamut mapping
      const float max_colorfulness = lookup_gamut(gamut_LUT, JCH[2]); // WARNING : this is M²
      const float max_chroma = (15.932993652962535f * powf(JCH[0] * L_white, 0.6523997524738018f)
                                * powf(max_colorfulness, 0.6007557017508491f) / L_white);
      const dt_aligned_pixel_t JCH_gamut_boundary = { JCH[0], max_chroma, JCH[2], 0.f };
      dt_aligned_pixel_t HSB_gamut_boundary;
      dt_UCS_JCH_to_HSB(JCH_gamut_boundary, HSB_gamut_boundary);

      // Clip saturation at constant brightness
      dt_aligned_pixel_t HSB = { HCB[0], (HCB[2] > 0.f) ? HCB[1] / HCB[2] : 0.f, HCB[2], 0.f };
      HSB[1] = soft_clip(HSB[1], 0.8f * HSB_gamut_boundary[1], HSB_gamut_boundary[1]);

      dt_UCS_HSB_to_JCH(HSB, JCH);
      dt_UCS_JCH_to_xyY(JCH, L_white, xyY);
      dt_xyY_to_XYZ(xyY, XYZ_D65);
    }

    // Project back to D50 pipeline RGB
    dt_aligned_pixel_t pix_out;
    dt_apply_transposed_color_matrix(XYZ_D65, output_matrix_trans, pix_out);

    /* The previous line is equivalent to :
      XYZ_D65_to_50(XYZ_D65, XYZ_D50);           // matrix product
      dot_product(XYZ_D50, XYZ_to_RGB, pix_out); // matrix product
    */

    if(mask_display)
    {
      // draw checkerboard
      dt_aligned_pixel_t color;
      const size_t i = (k / 4) / out_width;
      const size_t j = (k / 4) % out_width;
      if(i % checker_1 < i % checker_2)
      {
        if(j % checker_1 < j % checker_2)
          copy_pixel(color, d->checker_color_2);
        else
          copy_pixel(color, d->checker_color_1);
      }
      else
      {
        if(j % checker_1 < j % checker_2)
          copy_pixel(color, d->checker_color_1);
        else
          copy_pixel(color, d->checker_color_2);
      }

      float opacity = opacities[mask_type];
      const float opacity_comp = 1.0f - opacity;

      dt_vector_clipneg(pix_out);
      for_four_channels(c, aligned(pix_out, color:16))
        pix_out[c] = opacity_comp * color[c] + opacity * pix_out[c];
      pix_out[3] = 1.0f; // alpha is opaque, we need to preview it
    }
    else
    {
      dt_vector_clipneg(pix_out);
    }
    copy_pixel_nontemporal(out + k, pix_out);
  }
  dt_omploop_sfence();	// ensure all nontemporal writes complete before we use them
}


#if 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)
{
  const dt_iop_colorbalancergb_data_t *const d = piece->data;
  const dt_iop_colorbalancergb_global_data_t *const gd = self->global_data;
  const dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;

  cl_int err = DT_OPENCL_DEFAULT_ERROR;

  if(piece->colors != 4)
  {
    dt_control_log(_("colorbalance works only on RGB input"));
    return err;
  }

  const int devid = piece->pipe->devid;
  const int width = roi_in->width;
  const int height = roi_in->height;

  // Get working color profile
  const dt_iop_order_iccprofile_info_t *const work_profile
      = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
  if(work_profile == NULL) return err; // no point

  cl_mem dev_profile_info = NULL;
  cl_mem dev_profile_lut = NULL;
  dt_colorspaces_iccprofile_info_cl_t *profile_info_cl;
  cl_float *profile_lut_cl = NULL;

  cl_mem input_matrix_cl = NULL;
  cl_mem output_matrix_cl = NULL;
  cl_mem gamut_LUT_cl = NULL;
  cl_mem hue_rotation_matrix_cl = NULL;

  err = dt_ioppr_build_iccprofile_params_cl(work_profile, devid, &profile_info_cl, &profile_lut_cl,
                                            &dev_profile_info, &dev_profile_lut);
  if(err != CL_SUCCESS) goto error;

  // repack the matrices as flat AVX2-compliant matrice
  // work profile can't be fetched in commit_params since it is not yet initialised
  // work_profile->matrix_in === RGB_to_XYZ
  // work_profile->matrix_out === XYZ_to_RGB

  // Premultiply the input matrices

  /* What we do here is equivalent to :

    // go to CIE 1931 XYZ 2° D50
    dot_product(RGB, RGB_to_XYZ, XYZ_D50); // matrice product

    // chroma adapt D50 to D65
    XYZ_D50_to_65(XYZ_D50, XYZ_D65);       // matrice product

    // go to CIE 2006 LMS
    XYZ_to_LMS(XYZ_D65, LMS);              // matrice product

  * so we pre-multiply the 3 conversion matrices and operate only one matrix product
  */
  dt_colormatrix_t input_matrix;
  dt_colormatrix_t output_matrix;

  dt_colormatrix_mul(output_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in); // output_matrix used as temp buffer
  dt_colormatrix_mul(input_matrix, XYZ_D65_to_LMS_2006_D65, output_matrix);

  // Premultiply the output matrix

  /* What we do here is equivalent to :
    XYZ_D65_to_50(XYZ_D65, XYZ_D50);           // matrix product
    dot_product(XYZ_D50, XYZ_to_RGB, pix_out); // matrix product
  */

  dt_colormatrix_mul(output_matrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);

  input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), input_matrix);
  output_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), output_matrix);
  gamut_LUT_cl = dt_opencl_copy_host_to_device_constant(devid, LUT_ELEM * sizeof(float), d->gamut_LUT);

  // Size of the checker
  gboolean mask_display = FALSE;
  dt_iop_colorbalancergb_mask_data_t mask_type = MASK_NONE;
  if(self->dev->gui_attached && g && dt_pipe_is_full(piece->pipe))
  {
    dt_iop_gui_enter_critical_section(self);
    mask_display = g->mask_display;
    mask_type = g->mask_type;
    dt_iop_gui_leave_critical_section(self);
  }

  const int checker_1 = mask_display ? DT_PIXEL_APPLY_DPI(d->checker_size) : 0;
  const int checker_2 = 2 * checker_1;

  const float L_white = Y_to_dt_UCS_L_star(d->white_fulcrum);

  float hue_rotation_matrix[4]
    = { cosf(d->hue_angle), -sinf(d->hue_angle), sinf(d->hue_angle), cosf(d->hue_angle) };
  hue_rotation_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 4 * sizeof(float), hue_rotation_matrix);

  if(input_matrix_cl == NULL || output_matrix_cl == NULL || gamut_LUT_cl == NULL || hue_rotation_matrix_cl == NULL)
  {
    err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
    goto error;
  }

  err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_colorbalance_rgb, width, height,
    CLARG(dev_in), CLARG(dev_out),
    CLARG(width), CLARG(height), CLARG(dev_profile_info), CLARG(input_matrix_cl), CLARG(output_matrix_cl),
    CLARG(gamut_LUT_cl), CLARG(d->shadows_weight), CLARG(d->highlights_weight), CLARG(d->midtones_weight),
    CLARG(d->mask_grey_fulcrum), CLARG(d->hue_angle), CLARG(d->chroma_global), CLARG(d->chroma), CLARG(d->vibrance),
    CLARG(d->global), CLARG(d->shadows), CLARG(d->highlights), CLARG(d->midtones), CLARG(d->white_fulcrum),
    CLARG(d->midtones_Y), CLARG(d->grey_fulcrum), CLARG(d->contrast), CLARG(d->brilliance_global),
    CLARG(d->brilliance), CLARG(d->saturation_global), CLARG(d->saturation), CLARG(mask_display), CLARG(mask_type),
    CLARG(checker_1), CLARG(checker_2), CLARG(d->checker_color_1), CLARG(d->checker_color_2), CLARG(L_white),
    CLARG(d->saturation_formula), CLARG(hue_rotation_matrix_cl));

error:
  dt_ioppr_free_iccprofile_params_cl(&profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut);
  dt_opencl_release_mem_object(input_matrix_cl);
  dt_opencl_release_mem_object(output_matrix_cl);
  dt_opencl_release_mem_object(gamut_LUT_cl);
  dt_opencl_release_mem_object(hue_rotation_matrix_cl);
  return err;
}

void init_global(dt_iop_module_so_t *self)
{
  const int program = 8; // extended.cl in programs.conf
  dt_iop_colorbalancergb_global_data_t *gd = malloc(sizeof(dt_iop_colorbalancergb_global_data_t));

  self->data = gd;
  gd->kernel_colorbalance_rgb = dt_opencl_create_kernel(program, "colorbalancergb");
}


void cleanup_global(dt_iop_module_so_t *self)
{
  const dt_iop_colorbalancergb_global_data_t *gd = self->data;
  dt_opencl_free_kernel(gd->kernel_colorbalance_rgb);
  free(self->data);
  self->data = NULL;
}
#endif

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

  d->checker_color_1[0] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/red"), 0.f, 1.f);
  d->checker_color_1[1] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/green"), 0.f, 1.f);
  d->checker_color_1[2] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/blue"), 0.f, 1.f);
  d->checker_color_1[3] = 1.f;

  d->checker_color_2[0] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/red"), 0.f, 1.f);
  d->checker_color_2[1] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/green"), 0.f, 1.f);
  d->checker_color_2[2] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/blue"), 0.f, 1.f);
  d->checker_color_2[3] = 1.f;

  d->checker_size = MAX(dt_conf_get_int("plugins/darkroom/colorbalancergb/checker/size"), 2);

  d->vibrance = p->vibrance;
  d->contrast = 1.0f + p->contrast; // that limits the user param range to [-1, 1], but it seems enough
  d->grey_fulcrum = p->grey_fulcrum;

  d->chroma_global = p->chroma_global;
  d->chroma[0] = p->chroma_shadows;
  d->chroma[1] = p->chroma_midtones;
  d->chroma[2] = p->chroma_highlights;
  d->chroma[3] = 0.f;

  d->saturation_global = p->saturation_global;
  d->saturation[0] = p->saturation_shadows;
  d->saturation[1] = p->saturation_midtones;
  d->saturation[2] = p->saturation_highlights;
  d->saturation[3] = 0.f;

  d->brilliance_global = p->brilliance_global;
  d->brilliance[0] = p->brilliance_shadows;
  d->brilliance[1] = p->brilliance_midtones;
  d->brilliance[2] = p->brilliance_highlights;
  d->brilliance[3] = 0.f;

  d->hue_angle = deg2radf(p->hue_angle);

  // measure the grading RGB of a pure white
  const dt_aligned_pixel_t Ych_norm = { 1.f, 0.f, 1.f, 0.f };
  dt_aligned_pixel_t RGB_norm = { 0.f };
  Ych_to_gradingRGB(Ych_norm, RGB_norm);
  dt_aligned_pixel_t Ych;

  // global
  {
    make_Ych(1.f, p->global_C, CONVENTIONAL_DEG_TO_YRG_RAD(p->global_H), Ych);
    Ych_to_gradingRGB(Ych, d->global);
    for(size_t c = 0; c < 4; c++) d->global[c] = (d->global[c] - RGB_norm[c]) + RGB_norm[c] * p->global_Y;
  }

  // shadows
  {
    make_Ych(1.f, p->shadows_C, CONVENTIONAL_DEG_TO_YRG_RAD(p->shadows_H), Ych);
    Ych_to_gradingRGB(Ych, d->shadows);
    for(size_t c = 0; c < 4; c++) d->shadows[c] = 1.f + (d->shadows[c] - RGB_norm[c]) + p->shadows_Y;
    d->shadows_weight = 2.f + p->shadows_weight * 2.f;
  }

  // highlights
  {
    make_Ych(1.f, p->highlights_C, CONVENTIONAL_DEG_TO_YRG_RAD(p->highlights_H), Ych);
    Ych_to_gradingRGB(Ych, d->highlights);
    for(size_t c = 0; c < 4; c++) d->highlights[c] = 1.f + (d->highlights[c] - RGB_norm[c]) + p->highlights_Y;
    d->highlights_weight = 2.f + p->highlights_weight * 2.f;
  }

  // midtones
  {
    make_Ych(1.f, p->midtones_C, CONVENTIONAL_DEG_TO_YRG_RAD(p->midtones_H), Ych);
    Ych_to_gradingRGB(Ych, d->midtones);
    for(size_t c = 0; c < 4; c++) d->midtones[c] = 1.f / (1.f + (d->midtones[c] - RGB_norm[c]));
    d->midtones_Y = 1.f / (1.f + p->midtones_Y);
    d->white_fulcrum = exp2f(p->white_fulcrum);
    d->midtones_weight = sqf(d->shadows_weight) * sqf(d->highlights_weight) /
      (sqf(d->shadows_weight) + sqf(d->highlights_weight));
    d->mask_grey_fulcrum = powf(p->mask_grey_fulcrum, 0.4101205819200422f);
  }

  if(p->saturation_formula != d->saturation_formula) d->lut_inited = FALSE;
  d->saturation_formula = p->saturation_formula;

  // Check if the RGB working profile has changed in pipe
  // WARNING: this function is not triggered upon working profile change,
  // so the gamut boundaries are wrong until we change some param in this module
  dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
  if(work_profile == NULL) return;
  if(work_profile != d->work_profile)
  {
    d->lut_inited = FALSE;
    d->work_profile = work_profile;
  }

  // find the maximum chroma allowed by the current working gamut in conjunction to hue
  // this will be used to prevent users to mess up their images by pushing chroma out of gamut
  if(!d->lut_inited)
  {
    // Premultiply both matrices to go from D50 pipeline RGB to D65 XYZ in a single matrix dot product
    // instead of D50 pipeline to D50 XYZ (work_profile->matrix_in) and then D50 XYZ to D65 XYZ
    dt_colormatrix_t input_matrix;
    dt_colormatrix_mul(input_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
    float *gamut_LUT = d->gamut_LUT;
    // make RGB values vary between [0; 1] in working space, convert to Ych and get the max(c(h)))
    if(p->saturation_formula == DT_COLORBALANCE_SATURATION_JZAZBZ)
    {
      float *const restrict sampler = dt_calloc_align_float(LUT_ELEM);
      DT_OMP_FOR(reduction(max: sampler[:LUT_ELEM]) collapse(3))
      for(size_t r = 0; r < STEPS; r++)
        for(size_t g = 0; g < STEPS; g++)
          for(size_t b = 0; b < STEPS; b++)
          {
            const dt_aligned_pixel_t rgb = { (float)r / (float)(STEPS - 1), (float)g / (float)(STEPS - 1),
                                            (float)b / (float)(STEPS - 1), 0.f };
            dt_aligned_pixel_t XYZ = { 0.f };
            float saturation = 0.f;
            float hue = 0.f;

            dot_product(rgb, input_matrix, XYZ); // Go from D50 pipeline RGB to D65 XYZ in one step

            dt_aligned_pixel_t Jab, Jch;
            dt_XYZ_2_JzAzBz(XYZ, Jab);           // this one expects D65 XYZ

            Jch[0] = Jab[0];
            Jch[1] = dt_fast_hypotf(Jab[2], Jab[1]);
            Jch[2] = atan2f(Jab[2], Jab[1]);

            saturation = (Jch[0] > 0.f) ? Jch[1] / Jch[0] : 0.f;
            hue = Jch[2];

            int index = roundf((LUT_ELEM - 1) * (hue + M_PI_F) / DT_2PI_F);
            index += (index < 0) ? LUT_ELEM : 0;
            index -= (index >= LUT_ELEM) ? LUT_ELEM : 0;
            sampler[index] = fmaxf(sampler[index], saturation);
          }

     // anti-aliasing on the LUT (simple 5-taps 1D box average)
      for(size_t k = 2; k < LUT_ELEM - 2; k++)
        d->gamut_LUT[k] = (sampler[k - 2] + sampler[k - 1] + sampler[k] + sampler[k + 1] + sampler[k + 2]) / 5.f;

      // handle bounds
      d->gamut_LUT[0] = (sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2]) / 5.f;
      d->gamut_LUT[1] = (sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2] + sampler[3]) / 5.f;
      d->gamut_LUT[LUT_ELEM - 1] = (sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1]) / 5.f;
      d->gamut_LUT[LUT_ELEM - 2] = (sampler[LUT_ELEM - 4] + sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
      dt_free_align(sampler);
    }
    else if(p->saturation_formula == DT_COLORBALANCE_SATURATION_DTUCS)
      dt_UCS_22_build_gamut_LUT(input_matrix, gamut_LUT);

    d->lut_inited = TRUE;
  }
}

void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
  piece->data = dt_calloc1_align_type(dt_iop_colorbalancergb_data_t);
  dt_iop_colorbalancergb_data_t *d = piece->data;
  d->gamut_LUT = dt_alloc_align_float(LUT_ELEM);
  d->lut_inited = FALSE;
  d->work_profile = NULL;
}

void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
  const dt_iop_colorbalancergb_data_t *d = piece->data;
  if(d->gamut_LUT) dt_free_align(d->gamut_LUT);
  dt_free_align(piece->data);
  piece->data = NULL;
}

void pipe_RGB_to_Ych(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, const dt_aligned_pixel_t RGB,
                     dt_aligned_pixel_t Ych)
{
  const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, pipe);
  if(work_profile == NULL) return; // no point

  dt_aligned_pixel_t XYZ_D50 = { 0.f };
  dt_aligned_pixel_t XYZ_D65 = { 0.f };

  dt_ioppr_rgb_matrix_to_xyz(RGB, XYZ_D50, work_profile->matrix_in_transposed, work_profile->lut_in,
                             work_profile->unbounded_coeffs_in, work_profile->lutsize,
                             work_profile->nonlinearlut);
  XYZ_D50_to_D65(XYZ_D50, XYZ_D65);
  XYZ_to_Ych(XYZ_D65, Ych);
}


void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker,
                        dt_dev_pixelpipe_t *pipe)
{
  const dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
  dt_iop_colorbalancergb_params_t *p = self->params;

  dt_aligned_pixel_t Ych = { 0.f };
  dt_aligned_pixel_t max_Ych = { 0.f };
  pipe_RGB_to_Ych(self, pipe, (const float *)self->picked_color, Ych);
  pipe_RGB_to_Ych(self, pipe, (const float *)self->picked_color_max, max_Ych);
  const float picked_hue = get_hue_angle_from_Ych(Ych);
  const GdkModifierType state = dt_key_modifier_state();
  const float hue = (state & GDK_CONTROL_MASK) ? YRG_RAD_TO_CONVENTIONAL_DEG(picked_hue) : YRG_RAD_TO_CONVENTIONAL_DEG(picked_hue) + 180.f;    //take the current or opponent color

  DT_ENTER_GUI_UPDATE();
  if(picker == g->global_H)
  {
    p->global_H = hue;
    p->global_C = Ych[1] * Ych[0];
    dt_bauhaus_slider_set(g->global_H, p->global_H);
    dt_bauhaus_slider_set(g->global_C, p->global_C);
  }
  else if(picker == g->shadows_H)
  {
    p->shadows_H = hue;
    p->shadows_C = Ych[1] * Ych[0];
    dt_bauhaus_slider_set(g->shadows_H, p->shadows_H);
    dt_bauhaus_slider_set(g->shadows_C, p->shadows_C);
  }
  else if(picker == g->midtones_H)
  {
    p->midtones_H = hue;
    p->midtones_C = Ych[1] * Ych[0];
    dt_bauhaus_slider_set(g->midtones_H, p->midtones_H);
    dt_bauhaus_slider_set(g->midtones_C, p->midtones_C);
  }
  else if(picker == g->highlights_H)
  {
    p->highlights_H = hue;
    p->highlights_C = Ych[1] * Ych[0];
    dt_bauhaus_slider_set(g->highlights_H, p->highlights_H);
    dt_bauhaus_slider_set(g->highlights_C, p->highlights_C);
  }
  else if(picker == g->white_fulcrum)
  {
    p->white_fulcrum = log2f(max_Ych[0]);
    dt_bauhaus_slider_set(g->white_fulcrum, p->white_fulcrum);
  }
  else if(picker == g->grey_fulcrum)
  {
    p->grey_fulcrum = Ych[0];
    dt_bauhaus_slider_set(g->grey_fulcrum, p->grey_fulcrum);
  }
  else
    dt_print(DT_DEBUG_ALWAYS, "[colorbalancergb] unknown color picker");
  DT_LEAVE_GUI_UPDATE();

  gui_changed(self, picker, NULL);
  dt_dev_add_history_item(darktable.develop, self, TRUE);
}


static void _YchToRGB(dt_aligned_pixel_t *RGB_out, const float chroma, const float hue,
                      const dt_iop_order_iccprofile_info_t *output_profile,
                      const dt_colormatrix_t output_matrix_LMS_to_RGB)
{
  dt_aligned_pixel_t RGB_linear = { 0.f };
  dt_aligned_pixel_t Ych;
  make_Ych(0.75f, chroma, hue, Ych);
  dt_aligned_pixel_t XYZ_D65 = { 0.f };
  dt_aligned_pixel_t XYZ_D50 = { 0.f };
  Ych_to_XYZ(Ych, XYZ_D65);
  XYZ_D65_to_D50(XYZ_D65, XYZ_D50);
  dt_apply_transposed_color_matrix(XYZ_D50, output_profile->matrix_out_transposed, RGB_linear);
  // normalize to the brightest value available at this hue and chroma
  const float max_RGB = max3f(RGB_linear);
  for_each_channel(c) RGB_linear[c] = MAX(RGB_linear[c] / max_RGB, 0.f);
  // Apply nonlinear LUT if necessary
  if(output_profile->nonlinearlut)
    dt_ioppr_apply_trc(RGB_linear, *RGB_out, output_profile->lut_out, output_profile->unbounded_coeffs_out,
                       output_profile->lutsize);
  else
    memcpy(*RGB_out, RGB_linear, sizeof(RGB_linear));
}

static void paint_chroma_slider(const dt_iop_order_iccprofile_info_t *output_profile,
                                const dt_colormatrix_t output_matrix_LMS_to_RGB,
                                GtkWidget *w, const float hue)
{
  const float x_min = 0;
  const float x_max = 1;
  const float x_range = x_max - x_min;

  const float h = CONVENTIONAL_DEG_TO_YRG_RAD(hue);
  const float cos_h = cosf(h);
  const float sin_h = sinf(h);
  // Find max available chroma at this hue without negative RGB
  const float max_chroma = Ych_max_chroma_without_negatives(output_matrix_LMS_to_RGB, cos_h, sin_h);

  // Varies x in range around current y param
  for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
  {
    const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
    const float x = MIN(x_min + stop * x_range, max_chroma);

    dt_aligned_pixel_t RGB;
    _YchToRGB(&RGB, x, h, output_profile, output_matrix_LMS_to_RGB);
    dt_bauhaus_slider_set_stop(w, stop, RGB[0], RGB[1], RGB[2]);
  }

  gtk_widget_queue_draw(w);
}


static void paint_hue_sliders(const dt_iop_order_iccprofile_info_t *output_profile,
                              const dt_colormatrix_t output_matrix_LMS_to_RGB,
                              const dt_iop_colorbalancergb_gui_data_t *const g)
{
  for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
  {
    const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
    const float h = CONVENTIONAL_DEG_TO_YRG_RAD(stop * (360.f));
    const float max_chroma = Ych_max_chroma_without_negatives(output_matrix_LMS_to_RGB, cosf(h), sinf(h));
    dt_aligned_pixel_t RGB;
    _YchToRGB(&RGB, MIN(0.2f, max_chroma), h, output_profile, output_matrix_LMS_to_RGB);
    dt_bauhaus_slider_set_stop(g->global_H, stop, RGB[0], RGB[1], RGB[2]);
    dt_bauhaus_slider_set_stop(g->shadows_H, stop, RGB[0], RGB[1], RGB[2]);
    dt_bauhaus_slider_set_stop(g->highlights_H, stop, RGB[0], RGB[1], RGB[2]);
    dt_bauhaus_slider_set_stop(g->midtones_H, stop, RGB[0], RGB[1], RGB[2]);
  }
}

void gui_focus(dt_iop_module_t *self, gboolean in)
{
  if(!in)
  {
    dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
    const gboolean was_mask = g->mask_display;
    g->mask_display = FALSE;
    g->mask_type = MASK_NONE;
    dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->shadows_weight), FALSE);
    dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->mask_grey_fulcrum), FALSE);
    dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->highlights_weight), FALSE);
    if(was_mask)
      dt_iop_refresh_center(self);
  }
}

static void mask_callback(GtkWidget *togglebutton, dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();
  dt_iop_request_focus(self);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);

  dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;

  // if blend module is displaying mask do not display it here

  const gboolean mask_display = self->request_mask_display
                        ? FALSE
                        : dt_bauhaus_widget_get_quad_active(GTK_WIDGET(togglebutton));

  if(self->request_mask_display)
    dt_control_log(_("cannot display masks when the blending mask is displayed"));

  dt_iop_gui_enter_critical_section(self);
  g->mask_display = mask_display;
  if(g->mask_display)
  {
    if(togglebutton == g->shadows_weight) g->mask_type = MASK_SHADOWS;
    if(togglebutton == g->mask_grey_fulcrum) g->mask_type = MASK_MIDTONES;
    if(togglebutton == g->highlights_weight) g->mask_type = MASK_HIGHLIGHTS;
  }
  else
  {
    g->mask_type = MASK_NONE;
  }
  const dt_iop_colorbalancergb_mask_data_t mask_type = g->mask_type;
  dt_iop_gui_leave_critical_section(self);

  dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->shadows_weight), mask_type == MASK_SHADOWS);
  dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->mask_grey_fulcrum), mask_type == MASK_MIDTONES);
  dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->highlights_weight), mask_type == MASK_HIGHLIGHTS);

  dt_iop_refresh_center(self);
}


static gboolean dt_iop_tonecurve_draw(GtkWidget *widget, cairo_t *crf, const dt_iop_module_t *self)
{
  const dt_iop_colorbalancergb_params_t *p = self->params;
  const float shadows_weight = 2.f + p->shadows_weight * 2.f;
  const float highlights_weight = 2.f + p->highlights_weight * 2.f;

  // Cache the graph objects to avoid recomputing all the view at each redraw
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  GtkStyleContext *context = gtk_widget_get_style_context(widget);

  cairo_surface_t *cst =
    dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
  PangoFontDescription *desc = dt_gui_get_font();
  cairo_t *cr = cairo_create(cst);
  PangoLayout *layout = pango_cairo_create_layout(cr);

  const gint font_size = pango_font_description_get_size(desc);
  pango_font_description_set_size(desc, 0.95 * font_size);
  pango_layout_set_font_description(layout, desc);
  pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);

  char text[256];

  // Get the text line height for spacing
  PangoRectangle ink;
  snprintf(text, sizeof(text), "X");
  pango_layout_set_text(layout, text, -1);
  pango_layout_get_pixel_extents(layout, &ink, NULL);
  const float line_height = ink.height;

  const float inset = DT_PIXEL_APPLY_DPI(4);
  const float margin_top = inset;
  const float margin_bottom = line_height + 2 * inset;
  const float margin_left = line_height + inset;
  const float margin_right = 0;

  const float graph_width = allocation.width - margin_right - margin_left;   // align the right border on sliders
  const float graph_height = allocation.height - margin_bottom - margin_top; // give room to nodes

  gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height);

  // draw x gradient as axis legend
  cairo_pattern_t *grad;
  grad = cairo_pattern_create_linear(margin_left, 0.0, graph_width, 0.0);
  dt_cairo_perceptual_gradient(grad, 1.0);
  cairo_set_line_width(cr, 0.0);
  cairo_rectangle(cr, margin_left, graph_height + 2 * inset, graph_width, line_height);
  cairo_set_source(cr, grad);
  cairo_fill(cr);
  cairo_pattern_destroy(grad);

  // draw y gradient as axis legend
  const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, line_height);
  unsigned char *data = malloc(stride * graph_height);
  if(!data)
  {
     cairo_destroy(cr);
     cairo_surface_destroy(cst);
     return TRUE;
  }

  cairo_surface_t *surface = cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, (size_t)line_height, (size_t)graph_height, stride);

  const size_t checker_1 = DT_PIXEL_APPLY_DPI(6);
  const size_t checker_2 = 2 * checker_1;

  DT_OMP_FOR(collapse(2))
  for(size_t i = 0; i < (size_t)graph_height; i++)
    for(size_t j = 0; j < (size_t)line_height; j++)
    {
      const size_t k = ((i * (size_t)line_height) + j) * 4;
      unsigned char color;
      const float alpha = (float)i / graph_height;
      if(i % checker_1 < i % checker_2)
      {
        if(j % checker_1 < j % checker_2) color = 150;
        else color = 100;
      }
      else
      {
        if(j % checker_1 < j % checker_2) color = 100;
        else color = 150;
      }

      for(size_t c = 0; c < 4; ++c) data[k + c] = color * alpha;
      data[k+3] = alpha * 255;
    }

  cairo_set_source_surface(cr, surface, 0, margin_top);
  cairo_paint(cr);
  free(data);
  cairo_surface_destroy(surface);

  // set the graph as the origin of the coordinates
  cairo_translate(cr, margin_left, margin_top);
  cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);

  set_color(cr, darktable.bauhaus->graph_bg);
  cairo_rectangle(cr, 0, 0, graph_width, graph_height);
  cairo_fill_preserve(cr);
  cairo_clip(cr);

  // from https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.583.3007&rep=rep1&type=pdf
  const float midtones_weight
      = sqf(shadows_weight) * sqf(highlights_weight) / (sqf(shadows_weight) + sqf(highlights_weight));
  const float mask_grey_fulcrum = powf(p->mask_grey_fulcrum, 0.4101205819200422f);

  float *LUT[3];
  for(size_t c = 0; c < 3; c++) LUT[c] = dt_alloc_align_float(LUT_ELEM);

  DT_OMP_FOR()
  for(size_t k = 0 ; k < LUT_ELEM; k++)
  {
    const float Y = k / (float)(LUT_ELEM - 1);
    dt_aligned_pixel_t output;
    opacity_masks(Y, shadows_weight, highlights_weight, midtones_weight, mask_grey_fulcrum, output, NULL);
    for(size_t c = 0; c < 3; c++) LUT[c][k] = output[c];
  }

  const GdkRGBA fg_color = darktable.bauhaus->graph_fg;
  cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));

  for(size_t c = 0; c < 3; c++)
  {
    const GdkRGBA line_color = { fg_color.red * (1. - (2 - c) / 4.),
                           fg_color.green * (1. - (2 - c) / 4.),
                           fg_color.blue * (1. - (2 - c) / 4.),
                           fg_color.alpha };
    set_color(cr, line_color);

    cairo_move_to(cr, 0, (1.f - LUT[c][0]) * graph_height);
    for(size_t k = 0; k < LUT_ELEM; k++)
    {
      const float x = (float)k / (float)(LUT_ELEM - 1) * graph_width;
      const float y = (1.f - LUT[c][k]) * graph_height;
      cairo_line_to(cr, x, y);
    }
    cairo_stroke(cr);
  }

  for(size_t c = 0; c < 3; c++) dt_free_align(LUT[c]);

  cairo_restore(cr);

  // restore font size
  pango_font_description_set_size(desc, font_size);
  pango_layout_set_font_description(layout, desc);

  cairo_destroy(cr);
  cairo_set_source_surface(crf, cst, 0, 0);
  cairo_paint(crf);
  cairo_surface_destroy(cst);
  g_object_unref(layout);
  pango_font_description_free(desc);
  return FALSE;
}


static void checker_1_picker_callback(GtkColorButton *widget, const dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();

  GdkRGBA color;
  gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &color);
  dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/red", color.red);
  dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/green", color.green);
  dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/blue", color.blue);
  dt_iop_refresh_center(self);
}


static void checker_2_picker_callback(GtkColorButton *widget, const dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();

  GdkRGBA color;
  gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &color);
  dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/red", color.red);
  dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/green", color.green);
  dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/blue", color.blue);
  dt_iop_refresh_center(self);
}


static void checker_size_callback(GtkWidget *widget, const dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();
  const size_t size = dt_bauhaus_slider_get(widget);
  dt_conf_set_int("plugins/darkroom/colorbalancergb/checker/size", size);
  dt_iop_refresh_center(self);
}


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

  // Prepare data for gamut mapping slider backgrounds
  // Make our best effort to find display profile. If not found
  // or it is not a matrix profile, fall back to sRGB.
  const dt_iop_order_iccprofile_info_t *output_profile = NULL;

  if(self->dev && self->dev->full.pipe)
    output_profile = dt_ioppr_get_pipe_output_profile_info(self->dev->full.pipe);

  if(!output_profile || !dt_is_valid_colormatrix(output_profile->matrix_out[0][0]))
  {
    output_profile = dt_ioppr_add_profile_info_to_list(self->dev, DT_COLORSPACE_SRGB, "",
                                                       DT_INTENT_RELATIVE_COLORIMETRIC);
  }

  // Prepare LMS 2006 -> RGB matrix for chroma clipping
  dt_colormatrix_t input_matrix = { { 0.f } };  // this is actually unused but is generated by below function anyway
  dt_colormatrix_t output_matrix = { { 0.f } };
  prepare_RGB_Yrg_matrices(output_profile, input_matrix, output_matrix);

  // Check if output profile is different than last time - will need to repaint
  // slider backgrounds.
  const gboolean output_profile_changed = output_profile != g->sliders_output_profile;

   DT_ENTER_GUI_UPDATE();

  if(output_profile_changed)
    paint_hue_sliders(output_profile, output_matrix, g);

  if(!w || w == g->global_H || output_profile_changed)
    paint_chroma_slider(output_profile, output_matrix, g->global_C, p->global_H);

  if(!w || w == g->shadows_H || output_profile_changed)
    paint_chroma_slider(output_profile, output_matrix, g->shadows_C, p->shadows_H);

  if(!w || w == g->midtones_H || output_profile_changed)
    paint_chroma_slider(output_profile, output_matrix, g->midtones_C, p->midtones_H);

  if(!w || w == g->highlights_H || output_profile_changed)
    paint_chroma_slider(output_profile, output_matrix, g->highlights_C, p->highlights_H);

  if(!w || w == g->shadows_weight || w == g->highlights_weight || w == g->mask_grey_fulcrum)
    gtk_widget_queue_draw(GTK_WIDGET(g->area));

  DT_LEAVE_GUI_UPDATE();

  g->sliders_output_profile = output_profile;
}

void gui_update(dt_iop_module_t *self)
{
  dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
  const dt_iop_colorbalancergb_params_t *p = self->params;

  dt_bauhaus_slider_set(g->hue_angle, p->hue_angle);
  dt_bauhaus_slider_set(g->vibrance, p->vibrance);
  dt_bauhaus_slider_set(g->contrast, p->contrast);

  dt_bauhaus_slider_set(g->chroma_global, p->chroma_global);
  dt_bauhaus_slider_set(g->chroma_highlights, p->chroma_highlights);
  dt_bauhaus_slider_set(g->chroma_midtones, p->chroma_midtones);
  dt_bauhaus_slider_set(g->chroma_shadows, p->chroma_shadows);

  dt_bauhaus_slider_set(g->saturation_global, p->saturation_global);
  dt_bauhaus_slider_set(g->saturation_highlights, p->saturation_highlights);
  dt_bauhaus_slider_set(g->saturation_midtones, p->saturation_midtones);
  dt_bauhaus_slider_set(g->saturation_shadows, p->saturation_shadows);

  dt_bauhaus_slider_set(g->brilliance_global, p->brilliance_global);
  dt_bauhaus_slider_set(g->brilliance_highlights, p->brilliance_highlights);
  dt_bauhaus_slider_set(g->brilliance_midtones, p->brilliance_midtones);
  dt_bauhaus_slider_set(g->brilliance_shadows, p->brilliance_shadows);

  dt_bauhaus_slider_set(g->global_C, p->global_C);
  dt_bauhaus_slider_set(g->global_H, p->global_H);
  dt_bauhaus_slider_set(g->global_Y, p->global_Y);

  dt_bauhaus_slider_set(g->shadows_C, p->shadows_C);
  dt_bauhaus_slider_set(g->shadows_H, p->shadows_H);
  dt_bauhaus_slider_set(g->shadows_Y, p->shadows_Y);
  dt_bauhaus_slider_set(g->shadows_weight, p->shadows_weight);

  dt_bauhaus_slider_set(g->midtones_C, p->midtones_C);
  dt_bauhaus_slider_set(g->midtones_H, p->midtones_H);
  dt_bauhaus_slider_set(g->midtones_Y, p->midtones_Y);
  dt_bauhaus_slider_set(g->white_fulcrum, p->white_fulcrum);

  dt_bauhaus_slider_set(g->highlights_C, p->highlights_C);
  dt_bauhaus_slider_set(g->highlights_H, p->highlights_H);
  dt_bauhaus_slider_set(g->highlights_Y, p->highlights_Y);
  dt_bauhaus_slider_set(g->highlights_weight, p->highlights_weight);

  dt_bauhaus_slider_set(g->mask_grey_fulcrum, p->mask_grey_fulcrum);
  dt_bauhaus_slider_set(g->grey_fulcrum, p->grey_fulcrum);
  dt_bauhaus_combobox_set(g->saturation_formula, p->saturation_formula);

  gui_changed(self, NULL, NULL);
  dt_iop_color_picker_reset(self, TRUE);

  dt_iop_gui_enter_critical_section(self);
  g->mask_display = FALSE;
  g->mask_type = MASK_NONE;
  dt_iop_gui_leave_critical_section(self);

  dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->shadows_weight), FALSE);
  dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->mask_grey_fulcrum), FALSE);
  dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->highlights_weight), FALSE);

  // Checkerboard mask preview preferences
  GdkRGBA color;
  color.alpha = 1.0f;
  color.red = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/red");
  color.green = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/green");
  color.blue = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/blue");

  gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->checker_color_1_picker), &color);

  color.red = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/red");
  color.green = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/green");
  color.blue = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/blue");

  gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->checker_color_2_picker), &color);

  dt_bauhaus_slider_set(g->checker_size, dt_conf_get_int("plugins/darkroom/colorbalancergb/checker/size"));
}


void gui_reset(dt_iop_module_t *self)
{
  //dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
  dt_iop_color_picker_reset(self, TRUE);
}

void gui_init(dt_iop_module_t *self)
{
  dt_iop_colorbalancergb_gui_data_t *g = IOP_GUI_ALLOC(colorbalancergb);
  dt_iop_gui_enter_critical_section(self);
  g->mask_display = FALSE;
  dt_iop_gui_leave_critical_section(self);

  dt_iop_module_t *sect = NULL;

  // start building top level widget
  static dt_action_def_t notebook_def = { };
  g->notebook = dt_ui_notebook_new(&notebook_def);
  dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), &notebook_def);

  // Page master
  self->widget = dt_ui_notebook_page(g->notebook, N_("master"), _("global grading"));

  g->hue_angle = dt_bauhaus_slider_from_params(self, "hue_angle");
  dt_bauhaus_slider_set_format(g->hue_angle, "°");
  gtk_widget_set_tooltip_text(g->hue_angle, _("rotate all hues by an angle, at the same luminance"));

  g->vibrance = dt_bauhaus_slider_from_params(self, "vibrance");
  dt_bauhaus_slider_set_soft_range(g->vibrance, -0.5, 0.5);
  dt_bauhaus_slider_set_digits(g->vibrance, 4);
  dt_bauhaus_slider_set_format(g->vibrance, "%");
  gtk_widget_set_tooltip_text(g->vibrance, _("increase colorfulness mostly on low-chroma colors"));

  g->contrast = dt_bauhaus_slider_from_params(self, "contrast");
  dt_bauhaus_slider_set_soft_range(g->contrast, -0.5, 0.5);
  dt_bauhaus_slider_set_digits(g->contrast, 4);
  dt_bauhaus_slider_set_format(g->contrast, "%");
  gtk_widget_set_tooltip_text(g->contrast, _("increase the contrast at constant chromaticity"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "linear chroma grading")));
  sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("chroma"));

  g->chroma_global = dt_bauhaus_slider_from_params(self, "chroma_global");
  dt_bauhaus_slider_set_soft_range(g->chroma_global, -0.5, 0.5);
  dt_bauhaus_slider_set_digits(g->chroma_global, 4);
  dt_bauhaus_slider_set_format(g->chroma_global, "%");
  gtk_widget_set_tooltip_text(g->chroma_global, _("increase colorfulness at same luminance globally"));

  g->chroma_shadows = dt_bauhaus_slider_from_params(sect, "chroma_shadows");
  dt_bauhaus_slider_set_digits(g->chroma_shadows, 4);
  dt_bauhaus_slider_set_format(g->chroma_shadows, "%");
  gtk_widget_set_tooltip_text(g->chroma_shadows, _("increase colorfulness at same luminance mostly in shadows"));

  g->chroma_midtones = dt_bauhaus_slider_from_params(sect, "chroma_midtones");
  dt_bauhaus_slider_set_digits(g->chroma_midtones, 4);
  dt_bauhaus_slider_set_format(g->chroma_midtones, "%");
  gtk_widget_set_tooltip_text(g->chroma_midtones, _("increase colorfulness at same luminance mostly in mid-tones"));

  g->chroma_highlights = dt_bauhaus_slider_from_params(sect, "chroma_highlights");
  dt_bauhaus_slider_set_digits(g->chroma_highlights, 4);
  dt_bauhaus_slider_set_format(g->chroma_highlights, "%");
  gtk_widget_set_tooltip_text(g->chroma_highlights, _("increase colorfulness at same luminance mostly in highlights"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "perceptual saturation grading")));
  sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("saturation"));

  g->saturation_global = dt_bauhaus_slider_from_params(self, "saturation_global");
  dt_bauhaus_slider_set_digits(g->saturation_global, 4);
  dt_bauhaus_slider_set_format(g->saturation_global, "%");
  gtk_widget_set_tooltip_text(g->saturation_global, _("add or remove saturation by an absolute amount"));

  g->saturation_shadows = dt_bauhaus_slider_from_params(sect, "saturation_shadows");
  dt_bauhaus_slider_set_digits(g->saturation_shadows, 4);
  dt_bauhaus_slider_set_format(g->saturation_shadows, "%");
  gtk_widget_set_tooltip_text(g->saturation_shadows, _("increase or decrease saturation proportionally to the original pixel saturation"));

  g->saturation_midtones= dt_bauhaus_slider_from_params(sect, "saturation_midtones");
  dt_bauhaus_slider_set_digits(g->saturation_midtones, 4);
  dt_bauhaus_slider_set_format(g->saturation_midtones, "%");
  gtk_widget_set_tooltip_text(g->saturation_midtones, _("increase or decrease saturation proportionally to the original pixel saturation"));

  g->saturation_highlights = dt_bauhaus_slider_from_params(sect, "saturation_highlights");
  dt_bauhaus_slider_set_digits(g->saturation_highlights, 4);
  dt_bauhaus_slider_set_format(g->saturation_highlights, "%");
  gtk_widget_set_tooltip_text(g->saturation_highlights, _("increase or decrease saturation proportionally to the original pixel saturation"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "perceptual brilliance grading")));
  sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("brilliance"));

  g->brilliance_global = dt_bauhaus_slider_from_params(self, "brilliance_global");
  dt_bauhaus_slider_set_digits(g->brilliance_global, 4);
  dt_bauhaus_slider_set_format(g->brilliance_global, "%");
  gtk_widget_set_tooltip_text(g->brilliance_global, _("add or remove brilliance by an absolute amount"));

  g->brilliance_shadows = dt_bauhaus_slider_from_params(sect, "brilliance_shadows");
  dt_bauhaus_slider_set_digits(g->brilliance_shadows, 4);
  dt_bauhaus_slider_set_format(g->brilliance_shadows, "%");
  gtk_widget_set_tooltip_text(g->brilliance_shadows, _("increase or decrease brilliance proportionally to the original pixel brilliance"));

  g->brilliance_midtones= dt_bauhaus_slider_from_params(sect, "brilliance_midtones");
  dt_bauhaus_slider_set_digits(g->brilliance_midtones, 4);
  dt_bauhaus_slider_set_format(g->brilliance_midtones, "%");
  gtk_widget_set_tooltip_text(g->brilliance_midtones, _("increase or decrease brilliance proportionally to the original pixel brilliance"));

  g->brilliance_highlights = dt_bauhaus_slider_from_params(sect, "brilliance_highlights");
  dt_bauhaus_slider_set_digits(g->brilliance_highlights, 4);
  dt_bauhaus_slider_set_format(g->brilliance_highlights, "%");
  gtk_widget_set_tooltip_text(g->brilliance_highlights, _("increase or decrease brilliance proportionally to the original pixel brilliance"));

  // Page 4-ways
  self->widget = dt_ui_notebook_page(g->notebook, N_("4 ways"), _("selective color grading"));

  const char *hue_picker_tooltip = _("pick opposite color from image\nctrl+click to pick selected color");

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "global offset")));
  sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("offset"));

  g->global_Y = dt_bauhaus_slider_from_params(sect, "global_Y");
  dt_bauhaus_slider_set_soft_range(g->global_Y, -0.05, 0.05);
  dt_bauhaus_slider_set_digits(g->global_Y, 4);
  dt_bauhaus_slider_set_format(g->global_Y, "%");
  gtk_widget_set_tooltip_text(g->global_Y, _("global luminance offset"));

  g->global_H = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(sect, "global_H"));
  dt_bauhaus_slider_set_feedback(g->global_H, 0);
  dt_bauhaus_slider_set_format(g->global_H, "°");
  gtk_widget_set_tooltip_text(g->global_H, _("hue of the global color offset"));
  dt_bauhaus_widget_set_quad_tooltip(g->global_H, hue_picker_tooltip);

  g->global_C = dt_bauhaus_slider_from_params(sect, "global_C");
  dt_bauhaus_slider_set_soft_range(g->global_C, 0., 0.01);
  dt_bauhaus_slider_set_digits(g->global_C, 4);
  dt_bauhaus_slider_set_format(g->global_C, "%");
  gtk_widget_set_tooltip_text(g->global_C, _("chroma of the global color offset"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "shadows lift")));
  sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("lift"));

  g->shadows_Y = dt_bauhaus_slider_from_params(sect, "shadows_Y");
  dt_bauhaus_slider_set_soft_range(g->shadows_Y, -1.0, 1.0);
  dt_bauhaus_slider_set_digits(g->shadows_Y, 4);
  dt_bauhaus_slider_set_format(g->shadows_Y, "%");
  gtk_widget_set_tooltip_text(g->shadows_Y, _("luminance gain in shadows"));

  g->shadows_H = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(sect, "shadows_H"));
  dt_bauhaus_slider_set_feedback(g->shadows_H, 0);
  dt_bauhaus_slider_set_format(g->shadows_H, "°");
  gtk_widget_set_tooltip_text(g->shadows_H, _("hue of the color gain in shadows"));
  dt_bauhaus_widget_set_quad_tooltip(g->shadows_H, hue_picker_tooltip);

  g->shadows_C = dt_bauhaus_slider_from_params(sect, "shadows_C");
  dt_bauhaus_slider_set_soft_range(g->shadows_C, 0., 0.5);
  dt_bauhaus_slider_set_digits(g->shadows_C, 4);
  dt_bauhaus_slider_set_format(g->shadows_C, "%");
  gtk_widget_set_tooltip_text(g->shadows_C, _("chroma of the color gain in shadows"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "highlights gain")));
  sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("gain"));

  g->highlights_Y = dt_bauhaus_slider_from_params(sect, "highlights_Y");
  dt_bauhaus_slider_set_soft_range(g->highlights_Y, -0.5, 0.5);
  dt_bauhaus_slider_set_digits(g->highlights_Y, 4);
  dt_bauhaus_slider_set_format(g->highlights_Y, "%");
  gtk_widget_set_tooltip_text(g->highlights_Y, _("luminance gain in highlights"));

  g->highlights_H = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(sect, "highlights_H"));
  dt_bauhaus_slider_set_feedback(g->highlights_H, 0);
  dt_bauhaus_slider_set_format(g->highlights_H, "°");
  gtk_widget_set_tooltip_text(g->highlights_H, _("hue of the color gain in highlights"));
  dt_bauhaus_widget_set_quad_tooltip(g->highlights_H, hue_picker_tooltip);

  g->highlights_C = dt_bauhaus_slider_from_params(sect, "highlights_C");
  dt_bauhaus_slider_set_soft_range(g->highlights_C, 0., 0.2);
  dt_bauhaus_slider_set_digits(g->highlights_C, 4);
  dt_bauhaus_slider_set_format(g->highlights_C, "%");
  gtk_widget_set_tooltip_text(g->highlights_C, _("chroma of the color gain in highlights"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "power")));
  sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("power"));

  g->midtones_Y = dt_bauhaus_slider_from_params(sect, "midtones_Y");
  dt_bauhaus_slider_set_soft_range(g->midtones_Y, -0.25, 0.25);
  dt_bauhaus_slider_set_digits(g->midtones_Y, 4);
  dt_bauhaus_slider_set_format(g->midtones_Y, "%");
  gtk_widget_set_tooltip_text(g->midtones_Y, _("luminance exponent in mid-tones"));

  g->midtones_H = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(sect, "midtones_H"));
  dt_bauhaus_slider_set_feedback(g->midtones_H, 0);
  dt_bauhaus_slider_set_format(g->midtones_H, "°");
  gtk_widget_set_tooltip_text(g->midtones_H, _("hue of the color exponent in mid-tones"));
  dt_bauhaus_widget_set_quad_tooltip(g->midtones_H, hue_picker_tooltip);

  g->midtones_C = dt_bauhaus_slider_from_params(sect, "midtones_C");
  dt_bauhaus_slider_set_soft_range(g->midtones_C, 0., 0.1);
  dt_bauhaus_slider_set_digits(g->midtones_C, 4);
  dt_bauhaus_slider_set_format(g->midtones_C, "%");
  gtk_widget_set_tooltip_text(g->midtones_C, _("chroma of the color exponent in mid-tones"));

  // Page masks
  self->widget = dt_ui_notebook_page(g->notebook, N_("masks"), _("isolate luminances"));

  g->saturation_formula = dt_bauhaus_combobox_from_params(self, "saturation_formula");
  gtk_widget_set_tooltip_text(g->saturation_formula,
                              _("choose in which uniform color space the saturation is computed"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "luminance ranges")));

  g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL,
                                               0,
                                               "plugins/darkroom/colorbalancergb/graphheight"));
  g_object_set_data(G_OBJECT(g->area), "iop-instance", self);
  dt_action_define_iop(self, NULL, N_("graph"), GTK_WIDGET(g->area), NULL);
  g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(dt_iop_tonecurve_draw), self);
  dt_gui_box_add(self->widget, GTK_WIDGET(g->area));

  g->shadows_weight = dt_bauhaus_slider_from_params(self, "shadows_weight");
  dt_bauhaus_slider_set_digits(g->shadows_weight, 4);
  dt_bauhaus_slider_set_format(g->shadows_weight, "%");
  gtk_widget_set_tooltip_text(g->shadows_weight, _("weight of the shadows over the whole tonal range"));
  dt_bauhaus_widget_set_quad(g->shadows_weight,
                             self,
                             dtgtk_cairo_paint_showmask,
                             TRUE,
                             mask_callback,
                             _("displays a shadows mask, overlaid as a checkerboard\n"
                               "the still-visible area of the image (not hidden by the mask) is the area\n"
                               "that will be affected by the shadows sliders in the other tabs"));

  g->mask_grey_fulcrum = dt_bauhaus_slider_from_params(self, "mask_grey_fulcrum");
  dt_bauhaus_slider_set_digits(g->mask_grey_fulcrum, 4);
  dt_bauhaus_slider_set_format(g->mask_grey_fulcrum, "%");
  gtk_widget_set_tooltip_text(g->mask_grey_fulcrum, _("position of the middle-gray reference for masking"));
  dt_bauhaus_widget_set_quad(g->mask_grey_fulcrum,
                             self,
                             dtgtk_cairo_paint_showmask,
                             TRUE,
                             mask_callback,
                             _("displays a mid-tones mask, overlaid as a checkerboard\n"
                               "the still-visible area of the image (not hidden by the mask) is the area\n"
                               "that will be affected by the mid-tones sliders in the other tabs"));

  g->highlights_weight = dt_bauhaus_slider_from_params(self, "highlights_weight");
  dt_bauhaus_slider_set_digits(g->highlights_weight, 4);
  dt_bauhaus_slider_set_format(g->highlights_weight, "%");
  gtk_widget_set_tooltip_text(g->highlights_weight, _("weights of highlights over the whole tonal range"));
  dt_bauhaus_widget_set_quad(g->highlights_weight,
                             self,
                             dtgtk_cairo_paint_showmask,
                             TRUE,
                             mask_callback,
                             _("displays a highlights mask, overlaid as a checkerboard\n"
                               "the still-visible area of the image (not hidden by the mask) is the area\n"
                               "that will be affected by the highlights sliders in the other tabs"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "threshold")));

  g->white_fulcrum = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "white_fulcrum"));
  dt_bauhaus_slider_set_soft_range(g->white_fulcrum, -2., +2.);
  dt_bauhaus_slider_set_format(g->white_fulcrum, _(" EV"));
  gtk_widget_set_tooltip_text(g->white_fulcrum, _("peak white luminance value used to normalize the power function"));

  g->grey_fulcrum = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "grey_fulcrum"));
  dt_bauhaus_slider_set_soft_range(g->grey_fulcrum, 0.1, 0.5);
  dt_bauhaus_slider_set_digits(g->grey_fulcrum, 4);
  dt_bauhaus_slider_set_format(g->grey_fulcrum, "%");
  gtk_widget_set_tooltip_text(g->grey_fulcrum, _("peak gray luminance value used to normalize the power function"));

  dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "mask preview settings")));

  g->checker_color_1_picker = gtk_color_button_new();
  gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->checker_color_1_picker), FALSE);
  gtk_color_button_set_title(GTK_COLOR_BUTTON(g->checker_color_1_picker), _("select color of the checkerboard from a swatch"));
  g_signal_connect(G_OBJECT(g->checker_color_1_picker), "color-set", G_CALLBACK(checker_1_picker_callback), self);

  g->checker_color_2_picker = gtk_color_button_new();
  gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->checker_color_2_picker), FALSE);
  gtk_color_button_set_title(GTK_COLOR_BUTTON(g->checker_color_2_picker), _("select color of the checkerboard from a swatch"));
  g_signal_connect(G_OBJECT(g->checker_color_2_picker), "color-set", G_CALLBACK(checker_2_picker_callback), self);

  g->checker_size = dt_bauhaus_slider_new_with_range(self, 2., 32., 0, 8., 0);
  dt_bauhaus_slider_set_format(g->checker_size, _(" px"));
  dt_bauhaus_widget_set_label(g->checker_size,  NULL, _("checkerboard size"));
  g_signal_connect(G_OBJECT(g->checker_size), "value-changed", G_CALLBACK(checker_size_callback), self);

  dt_gui_box_add(self->widget,
    dt_gui_hbox(dt_gui_expand(dt_ui_label_new(_("checkerboard color 1"))), g->checker_color_1_picker),
    dt_gui_hbox(dt_gui_expand(dt_ui_label_new(_("checkerboard color 2"))), g->checker_color_2_picker),
    g->checker_size);

  // Init the conf keys if they don't exist
  if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker1/red"))
    dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/red", 1.0f);
  if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker1/green"))
    dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/green", 1.0f);
  if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker1/blue"))
    dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/blue", 1.0f);

  if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker2/red"))
    dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/red", 0.18f);
  if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker2/green"))
    dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/green", 0.18f);
  if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker2/blue"))
    dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/blue", 0.18f);

  if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker/size"))
    dt_conf_set_int("plugins/darkroom/colorbalancergb/checker/size", 8);

  // paint backgrounds
  for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
  {
    const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
    const float Y = 0.f + stop;
    dt_bauhaus_slider_set_stop(g->global_Y, stop, Y, Y, Y);
    dt_bauhaus_slider_set_stop(g->shadows_Y, stop, Y, Y, Y);
    dt_bauhaus_slider_set_stop(g->highlights_Y, stop, Y, Y, Y);
    dt_bauhaus_slider_set_stop(g->midtones_Y, stop, Y, Y, Y);
  }

  // main widget is the notebook
  self->widget = GTK_WIDGET(g->notebook);
}

// 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