/*
This file is part of darktable,
Copyright (C) 2009-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 .
*/
#include
#include
#include
#include
#include "bauhaus/bauhaus.h"
#include "common/histogram.h"
#include "common/image_cache.h"
#include "common/mipmap_cache.h"
#include "common/opencl.h"
#include "control/control.h"
#include "develop/develop.h"
#include "develop/imageop.h"
#include "develop/imageop_math.h"
#include "develop/imageop_gui.h"
#include "develop/pixelpipe.h"
#include "dtgtk/paint.h"
#include "dtgtk/resetlabel.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"
#include "gui/presets.h"
#include "gui/color_picker_proxy.h"
#include "iop/iop_api.h"
// 'white' is the value that is mapped to 1.0 after exposure correction
#define exposure2white(x) exp2f(-(x))
#define white2exposure(x) -dt_log2f(fmaxf(1e-20f, x))
DT_MODULE_INTROSPECTION(7, dt_iop_exposure_params_t)
typedef enum dt_iop_exposure_mode_t
{
EXPOSURE_MODE_MANUAL, // $DESCRIPTION: "manual"
EXPOSURE_MODE_DEFLICKER // $DESCRIPTION: "automatic"
} dt_iop_exposure_mode_t;
typedef enum dt_spot_mode_t
{
DT_SPOT_MODE_CORRECT = 0,
DT_SPOT_MODE_MEASURE = 1,
DT_SPOT_MODE_LAST
} dt_spot_mode_t;
// uint16_t pixel can have any value in range [0, 65535], thus, there is
// 65536 possible values.
#define DEFLICKER_BINS_COUNT (UINT16_MAX + 1)
typedef struct dt_iop_exposure_params_t
{
dt_iop_exposure_mode_t mode; // $DEFAULT: EXPOSURE_MODE_MANUAL
float black; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "black level correction"
float exposure; // $MIN: -18.0 $MAX: 18.0 $DEFAULT: 0.0
float deflicker_percentile; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 50.0 $DESCRIPTION: "percentile"
float deflicker_target_level; // $MIN: -18.0 $MAX: 18.0 $DEFAULT: -4.0 $DESCRIPTION: "target level"
gboolean compensate_exposure_bias;// $DEFAULT: FALSE $DESCRIPTION: "compensate exposure bias"
gboolean compensate_hilite_pres; // $DEFAULT: TRUE $DESCRIPTION: "compensate highlight preservation"
} dt_iop_exposure_params_t;
typedef struct dt_iop_exposure_gui_data_t
{
GtkWidget *mode;
GtkWidget *black;
GtkStack *mode_stack;
GtkWidget *exposure;
GtkWidget *deflicker_percentile;
GtkWidget *deflicker_target_level;
GtkLabel *deflicker_used_EC;
GtkWidget *compensate_exposure_bias;
GtkWidget *compensate_hilite_preserv;
float deflicker_computed_exposure;
GtkWidget *spot_mode;
GtkWidget *lightness_spot;
GtkWidget *origin_spot, *target_spot;
GtkWidget *Lch_origin;
dt_gui_collapsible_section_t cs;
dt_aligned_pixel_t spot_RGB;
} dt_iop_exposure_gui_data_t;
typedef struct dt_iop_exposure_data_t
{
dt_iop_exposure_params_t params;
gboolean deflicker;
float black;
float scale;
// cache the raw source histogram per pipe: cleanup is serialized with
// processing, and image changes rebuild the pipe nodes
uint32_t *deflicker_histogram;
dt_dev_histogram_stats_t deflicker_histogram_stats;
} dt_iop_exposure_data_t;
typedef struct dt_iop_exposure_global_data_t
{
int kernel_exposure;
} dt_iop_exposure_global_data_t;
#define EXPOSURE_CORRECTION_UNDEFINED (-FLT_MAX)
const char *name()
{
return _("exposure");
}
const char** description(dt_iop_module_t *self)
{
return dt_iop_set_description
(self,
_("redo the exposure of the shot as if you were still in-camera\n"
"using a color-safe brightening similar to increasing ISO setting"),
_("corrective and creative"),
_("linear, RGB, scene-referred"),
_("linear, RGB"),
_("linear, RGB, scene-referred"));
}
int default_group()
{
return IOP_GROUP_BASIC | IOP_GROUP_TECHNICAL;
}
int flags()
{
return IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_SUPPORTS_BLENDING;
}
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;
}
static float _exposure_proxy_get_exposure(dt_iop_module_t *self);
static void _paint_hue(dt_iop_module_t *self);
static void _exposure_set_black(dt_iop_module_t *self, const float black);
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_exposure_params_v6_t
{
dt_iop_exposure_mode_t mode;
float black;
float exposure;
float deflicker_percentile;
float deflicker_target_level;
gboolean compensate_exposure_bias;
} dt_iop_exposure_params_v6_t;
typedef struct dt_iop_exposure_params_v7_t
{
dt_iop_exposure_mode_t mode;
float black;
float exposure;
float deflicker_percentile;
float deflicker_target_level;
gboolean compensate_exposure_bias;
gboolean compensate_hilite_pres;
} dt_iop_exposure_params_v7_t;
if(old_version == 2)
{
typedef struct dt_iop_exposure_params_v2_t
{
float black, exposure, gain;
} dt_iop_exposure_params_v2_t;
const dt_iop_exposure_params_v2_t *o = (dt_iop_exposure_params_v2_t *)old_params;
dt_iop_exposure_params_v6_t *n = malloc(sizeof(dt_iop_exposure_params_v6_t));
n->mode = EXPOSURE_MODE_MANUAL;
n->black = o->black;
n->exposure = o->exposure;
n->compensate_exposure_bias = FALSE;
n->deflicker_percentile = 50.0f;
n->deflicker_target_level = -4.0f;
*new_params = n;
*new_params_size = sizeof(dt_iop_exposure_params_v6_t);
*new_version = 6;
return 0;
}
if(old_version == 3)
{
typedef struct dt_iop_exposure_params_v3_t
{
float black, exposure;
gboolean deflicker;
float deflicker_percentile, deflicker_target_level;
} dt_iop_exposure_params_v3_t;
const dt_iop_exposure_params_v3_t *o = (dt_iop_exposure_params_v3_t *)old_params;
dt_iop_exposure_params_v6_t *n = malloc(sizeof(dt_iop_exposure_params_v6_t));
n->mode = o->deflicker ? EXPOSURE_MODE_DEFLICKER : EXPOSURE_MODE_MANUAL;
n->black = o->black;
n->exposure = o->exposure;
n->deflicker_percentile = o->deflicker_percentile;
n->deflicker_target_level = o->deflicker_target_level;
n->compensate_exposure_bias = FALSE;
*new_params = n;
*new_params_size = sizeof(dt_iop_exposure_params_v6_t);
*new_version = 6;
return 0;
}
if(old_version == 4)
{
typedef enum dt_iop_exposure_deflicker_histogram_source_t {
DEFLICKER_HISTOGRAM_SOURCE_THUMBNAIL,
DEFLICKER_HISTOGRAM_SOURCE_SOURCEFILE
} dt_iop_exposure_deflicker_histogram_source_t;
typedef struct dt_iop_exposure_params_v4_t
{
dt_iop_exposure_mode_t mode;
float black;
float exposure;
float deflicker_percentile, deflicker_target_level;
dt_iop_exposure_deflicker_histogram_source_t deflicker_histogram_source;
} dt_iop_exposure_params_v4_t;
const dt_iop_exposure_params_v4_t *o = (dt_iop_exposure_params_v4_t *)old_params;
dt_iop_exposure_params_v6_t *n = malloc(sizeof(dt_iop_exposure_params_v6_t));
n->mode = o->mode;
n->black = o->black;
n->exposure = o->exposure;
n->deflicker_percentile = o->deflicker_percentile;
n->deflicker_target_level = o->deflicker_target_level;
// deflicker_histogram_source is dropped. this does change output,
// but deflicker still was not publicly released at that point
n->compensate_exposure_bias = FALSE;
*new_params = n;
*new_params_size = sizeof(dt_iop_exposure_params_v6_t);
*new_version = 6;
return 0;
}
if(old_version == 5)
{
typedef struct dt_iop_exposure_params_v5_t
{
dt_iop_exposure_mode_t mode;
float black;
float exposure;
float deflicker_percentile, deflicker_target_level;
} dt_iop_exposure_params_v5_t;
const dt_iop_exposure_params_v5_t *o = (dt_iop_exposure_params_v5_t *)old_params;
dt_iop_exposure_params_v6_t *n = malloc(sizeof(dt_iop_exposure_params_v6_t));
n->mode = o->mode;
n->black = o->black;
n->exposure = o->exposure;
n->deflicker_percentile = o->deflicker_percentile;
n->deflicker_target_level = o->deflicker_target_level;
n->compensate_exposure_bias = FALSE;
*new_params = n;
*new_params_size = sizeof(dt_iop_exposure_params_v6_t);
*new_version = 6;
return 0;
}
if(old_version == 6)
{
const dt_iop_exposure_params_v6_t *o = (dt_iop_exposure_params_v6_t *)old_params;
dt_iop_exposure_params_v7_t *n = malloc(sizeof(dt_iop_exposure_params_v7_t));
n->mode = o->mode;
n->black = o->black;
n->exposure = o->exposure;
n->deflicker_percentile = o->deflicker_percentile;
n->deflicker_target_level = o->deflicker_target_level;
n->compensate_exposure_bias = o->compensate_exposure_bias;
n->compensate_hilite_pres = FALSE; // module did not compensate h.p. before version 7
*new_params = n;
*new_params_size = sizeof(dt_iop_exposure_params_v7_t);
*new_version = 7;
return 0;
}
return 1;
}
void init_presets(dt_iop_module_so_t *self)
{
self->pref_based_presets = TRUE;
dt_gui_presets_add_generic
(_("magic lantern defaults"), self->op,
self->version(),
&(dt_iop_exposure_params_t){.mode = EXPOSURE_MODE_DEFLICKER,
.black = 0.0f,
.exposure = 0.0f,
.deflicker_percentile = 50.0f,
.deflicker_target_level = -4.0f,
.compensate_exposure_bias = FALSE,
.compensate_hilite_pres = FALSE },
sizeof(dt_iop_exposure_params_t), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY);
const gboolean is_scene_referred = dt_is_scene_referred();
if(is_scene_referred)
{
// For scene-referred workflow, since filmic doesn't brighten as base curve does,
// we need an initial exposure boost. This preset has the same value as what is
// auto-applied (see reload_default below) for scene-referred workflow.
dt_gui_presets_add_generic
(_("scene-referred default"), self->op, self->version(),
NULL, 0,
TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
dt_gui_presets_update_format(BUILTIN_PRESET("scene-referred default"), self->op,
self->version(), FOR_RAW);
dt_gui_presets_update_autoapply(BUILTIN_PRESET("scene-referred default"),
self->op, self->version(), TRUE);
}
}
void reload_defaults(dt_iop_module_t *self)
{
dt_iop_exposure_params_t *d = self->default_params;
const gboolean scene_raw =
dt_image_is_rawprepare_supported(&self->dev->image_storage)
&& dt_is_scene_referred();
d->mode = EXPOSURE_MODE_MANUAL;
if(scene_raw && self->multi_priority == 0)
{
const gboolean mono = dt_image_is_monochrome(&self->dev->image_storage);
d->exposure = mono ? 0.0f : 0.7f;
d->black = mono ? 0.0f : -0.000244140625f;
d->compensate_exposure_bias = TRUE;
}
else
{
d->exposure = 0.0f;
d->black = 0.0f;
d->compensate_exposure_bias = FALSE;
}
// the new default is to compensate for highlight preservation mode,
// but ONLY if we're the first instance (to avoid multiple application)
d->compensate_hilite_pres = dt_iop_is_first_instance(self->dev->iop, self);
// both defaults above depend on the image and on the instance, so the
// widgets have to be told about them again, as for sliders and comboboxes
dt_iop_exposure_gui_data_t *g = self->gui_data;
if(g)
{
dt_bauhaus_toggle_set_default(g->compensate_exposure_bias,
d->compensate_exposure_bias);
dt_bauhaus_toggle_set_default(g->compensate_hilite_preserv,
d->compensate_hilite_pres);
}
}
static void _deflicker_prepare_histogram(dt_iop_module_t *self,
uint32_t **histogram,
dt_dev_histogram_stats_t *histogram_stats)
{
const dt_image_t *img = dt_image_cache_get(self->dev->image_storage.id, 'r');
dt_image_t image = *img;
dt_image_cache_read_release(img);
if(!img || image.buf_dsc.channels != 1 || image.buf_dsc.datatype != TYPE_UINT16) return;
dt_mipmap_buffer_t buf;
dt_mipmap_cache_get(&buf, self->dev->image_storage.id, DT_MIPMAP_FULL, DT_MIPMAP_BLOCKING, 'r');
if(!buf.buf)
{
dt_control_log(_("failed to get raw buffer from image `%s'"), image.filename);
dt_mipmap_cache_release(&buf);
return;
}
dt_dev_histogram_collection_params_t histogram_params = { 0 };
dt_histogram_roi_t histogram_roi = {.width = image.width,
.height = image.height,
// FIXME: get those from rawprepare IOP somehow !!!
.crop_x = image.crop_x,
.crop_y = image.crop_y,
.crop_right = image.crop_right,
.crop_bottom = image.crop_bottom };
histogram_params.roi = &histogram_roi;
histogram_params.bins_count = DEFLICKER_BINS_COUNT;
dt_histogram_helper(&histogram_params, histogram_stats, IOP_CS_RAW, IOP_CS_NONE,
buf.buf, histogram, NULL, FALSE, NULL);
dt_mipmap_cache_release(&buf);
}
/* input: 0 - 65535 (valid range: from black level to white level) */
/* output: -16 ... 0 */
static double _raw_to_ev(const uint32_t raw,
const uint32_t black_level,
const uint32_t white_level)
{
const uint32_t raw_max = white_level - black_level;
// we are working on data without black clipping,
// so we can get values which are lower than the black level !!!
const int64_t raw_val = MAX((int64_t)raw - (int64_t)black_level, 1);
const double raw_ev = -log2(raw_max) + log2(raw_val);
return raw_ev;
}
static void _compute_deflicker_correction(dt_iop_exposure_params_t *p,
dt_dev_pixelpipe_t *pipe,
const uint32_t *const histogram,
const dt_dev_histogram_stats_t *const histogram_stats,
float *correction)
{
// preserve caller's correction if we cannot compute anything
if(histogram == NULL) return;
const double thr
= CLAMP(((double)histogram_stats->pixels * (double)p->deflicker_percentile
/ (double)100.0), 0.0, (double)histogram_stats->pixels);
size_t n = 0;
uint32_t raw = 0;
for(size_t i = 0; i < histogram_stats->bins_count; i++)
{
n += histogram[i];
if((double)n >= thr)
{
raw = i;
break;
}
}
const double ev
= _raw_to_ev(raw, (uint32_t)pipe->dsc.rawprepare.raw_black_level,
pipe->dsc.rawprepare.raw_white_point);
*correction = p->deflicker_target_level - ev;
}
static gboolean _show_computed(gpointer user_data);
static void _process_common_setup(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_exposure_gui_data_t *g = self->gui_data;
dt_iop_exposure_data_t *d = piece->data;
d->black = d->params.black;
// the default is also the fallback for _compute_deflicker_correction below
float exposure = d->params.exposure;
if(d->deflicker)
{
if(!d->deflicker_histogram)
_deflicker_prepare_histogram(self, &d->deflicker_histogram,
&d->deflicker_histogram_stats);
_compute_deflicker_correction(&d->params, piece->pipe,
d->deflicker_histogram, &d->deflicker_histogram_stats,
&exposure);
// second, show computed correction in UI.
if(g && dt_pipe_is_preview(piece->pipe))
{
dt_iop_gui_enter_critical_section(self);
g->deflicker_computed_exposure = exposure;
dt_iop_gui_leave_critical_section(self);
g_idle_add(_show_computed, self);
}
}
const float white = exposure2white(exposure);
if(d->black >= white)
d->black = white - 0.01f;
d->scale = 1.0f / (white - d->black);
}
#ifdef HAVE_OPENCL
int process_cl(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
cl_mem dev_in,
cl_mem dev_out,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
dt_iop_exposure_data_t *d = piece->data;
dt_iop_exposure_global_data_t *gd = self->global_data;
_process_common_setup(self, piece);
cl_int err = DT_OPENCL_DEFAULT_ERROR;
const int devid = piece->pipe->devid;
const int width = roi_in->width;
const int height = roi_in->height;
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_exposure, width, height,
CLARG(dev_in), CLARG(dev_out),
CLARG(width), CLARG(height),
CLARG((d->black)), CLARG((d->scale)));
if(err != CL_SUCCESS) goto error;
for(int k = 0; k < 3; k++) piece->pipe->dsc.processed_maximum[k] *= d->scale;
error:
return err;
}
#endif
void process(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const void *const i,
void *const o,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
const dt_iop_exposure_data_t *const d = piece->data;
_process_common_setup(self, piece);
const int ch = piece->colors;
const float *const restrict in = (float*)i;
float *const restrict out = (float*)o;
const float black = d->black;
const float scale = d->scale;
const size_t npixels = (size_t)roi_out->width * roi_out->height;
DT_OMP_FOR_SIMD(aligned(in, out : 64))
for(size_t k = 0; k < ch * npixels; k++)
{
out[k] = (in[k] - black) * scale;
}
for(int k = 0; k < 3; k++)
piece->pipe->dsc.processed_maximum[k] *= d->scale;
}
static float _get_exposure_bias(const dt_iop_module_t *self)
{
float bias = 0.0f;
// just check that pointers exist and are initialized
if(self->dev && self->dev->image_storage.exif_exposure_bias)
bias = self->dev->image_storage.exif_exposure_bias;
// sanity checks, don't trust exif tags too much
if(bias != DT_EXIF_TAG_UNINITIALIZED)
return CLAMP(bias, -5.0f, 5.0f);
else
return 0.0f;
}
static float _get_highlight_bias(const dt_iop_module_t *self)
{
float bias = 0.0f;
// exif_highlight_preservation holds the exposure (EV) that the camera withheld
// in an HDR / dynamic-range / HLG tone mode; so we compensate for it in exposure.
// Per-camera detection happens in _check_highlight_preservation() in common/exif.cc.
if(self->dev && self->dev->image_storage.exif_highlight_preservation > 0.0f)
bias = self->dev->image_storage.exif_highlight_preservation;
// sanity checks, don't trust exif tags too much
if(bias != DT_EXIF_TAG_UNINITIALIZED)
return CLAMP(bias, -1.0f, 4.0f);
else
return 0.0f;
}
// The correction, in EV, that the module adds on top of the user's exposure parameter to
// account for the two biases recorded in EXIF.
// Both directions of the conversion (see below) go through this, so they
// cannot drift apart.
static inline float _exposure_compensation_ev(const dt_iop_module_t *const self,
const dt_iop_exposure_params_t *const p)
{
float compensation = 0.0f;
// compensate the correction the user dialed into the camera
if(p->compensate_exposure_bias)
compensation -= _get_exposure_bias(self);
// undo the underexposure the camera applied automatically
if(p->compensate_hilite_pres)
compensation += _get_highlight_bias(self);
return compensation;
}
// The total exposure adjustment the pipe applies in manual mode: the slider plus the
// exposure compensation. commit_params() and the proxy accessor both go through this,
// so the value other modules read cannot drift away from the one that is processed.
static inline float _total_adjustment_ev(const dt_iop_module_t *const self,
const dt_iop_exposure_params_t *const p)
{
return p->exposure + _exposure_compensation_ev(self, p);
}
// The inverse: the value to store in p->exposure (i.e. where to move the exposure slider)
// so that the pipe ends up applying `total_adjustment_ev`. The GUI needs it because it
// reasons in terms of the total adjustment - black has to stay below the white point
// derived from it - but writes the user parameter (slider value).
static inline float _required_exposure_slider_ev(const dt_iop_module_t *const self,
const dt_iop_exposure_params_t *const p,
const float total_adjustment_ev)
{
return total_adjustment_ev - _exposure_compensation_ev(self, p);
}
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_exposure_params_t *p = (dt_iop_exposure_params_t *)p1;
dt_iop_exposure_data_t *d = piece->data;
d->params.black = p->black;
d->params.exposure = _total_adjustment_ev(self, p);
d->params.deflicker_percentile = p->deflicker_percentile;
d->params.deflicker_target_level = p->deflicker_target_level;
d->deflicker = FALSE;
if(p->mode == EXPOSURE_MODE_DEFLICKER
&& dt_image_is_raw(&self->dev->image_storage)
&& self->dev->image_storage.buf_dsc.channels == 1
&& self->dev->image_storage.buf_dsc.datatype == TYPE_UINT16)
{
d->deflicker = TRUE;
}
}
void init_pipe(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
piece->data = calloc(1,sizeof(dt_iop_exposure_data_t));
}
void cleanup_pipe(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_exposure_data_t *d = piece->data;
dt_free_align(d->deflicker_histogram);
free(piece->data);
piece->data = NULL;
}
static void _autoexp_disable(dt_iop_module_t *self)
{
dt_iop_color_picker_reset(self, TRUE);
}
void gui_update(dt_iop_module_t *self)
{
dt_iop_exposure_gui_data_t *g = self->gui_data;
dt_iop_exposure_params_t *p = self->params;
if(!dt_image_is_raw(&self->dev->image_storage)
|| self->dev->image_storage.buf_dsc.channels != 1
|| self->dev->image_storage.buf_dsc.datatype != TYPE_UINT16)
{
gtk_widget_set_sensitive(GTK_WIDGET(g->mode), FALSE);
p->mode = EXPOSURE_MODE_MANUAL;
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
else
{
gtk_widget_set_sensitive(GTK_WIDGET(g->mode), TRUE);
}
dt_iop_color_picker_reset(self, TRUE);
dt_bauhaus_toggle_set(g->compensate_exposure_bias,
p->compensate_exposure_bias);
gchar *label = g_strdup_printf(_("compensate camera exposure (%+.1f EV)"),
_get_exposure_bias(self));
dt_bauhaus_widget_set_label_text(g->compensate_exposure_bias, label);
g_free(label);
const float hlbias = _get_highlight_bias(self);
dt_bauhaus_toggle_set(g->compensate_hilite_preserv,
p->compensate_hilite_pres);
/* xgettext:no-c-format */
label = g_strdup_printf(_("highlight preservation mode (%.1f EV)"), hlbias);
dt_bauhaus_widget_set_label_text(g->compensate_hilite_preserv, label);
g_free(label);
gtk_widget_set_visible(GTK_WIDGET(g->compensate_hilite_preserv), hlbias > 0.0f);
g->spot_RGB[0] = 0.f;
g->spot_RGB[1] = 0.f;
g->spot_RGB[2] = 0.f;
g->spot_RGB[3] = 0.f;
// get the saved params
dt_iop_gui_enter_critical_section(self);
const float lightness = dt_conf_get_float("darkroom/modules/exposure/lightness");
dt_bauhaus_slider_set(g->lightness_spot, lightness);
dt_iop_gui_leave_critical_section(self);
gtk_label_set_text(g->deflicker_used_EC, "");
dt_iop_gui_enter_critical_section(self);
g->deflicker_computed_exposure = EXPOSURE_CORRECTION_UNDEFINED;
dt_iop_gui_leave_critical_section(self);
switch(p->mode)
{
case EXPOSURE_MODE_DEFLICKER:
_autoexp_disable(self);
gtk_stack_set_visible_child_name(GTK_STACK(g->mode_stack), "deflicker");
break;
case EXPOSURE_MODE_MANUAL:
default:
gtk_stack_set_visible_child_name(GTK_STACK(g->mode_stack), "manual");
break;
}
dt_bauhaus_combobox_set(g->spot_mode, 0);
dt_gui_update_collapsible_section(&g->cs);
}
void init_global(dt_iop_module_so_t *self)
{
const int program = 2; // from programs.conf: basic.cl
dt_iop_exposure_global_data_t *gd = calloc(1,sizeof(dt_iop_exposure_global_data_t));
self->data = gd;
gd->kernel_exposure = dt_opencl_create_kernel(program, "exposure");
}
void cleanup_global(dt_iop_module_so_t *self)
{
dt_iop_exposure_global_data_t *gd = self->data;
dt_opencl_free_kernel(gd->kernel_exposure);
free(self->data);
self->data = NULL;
}
// Set the exposure parameter (slider value) such that 'white' is mapped to 1.0 after
// commit_params() re-applies the compensations. Callers work in that domain
// because p->black does too - black is never compensated - so the black clamps can
// compare the two directly.
static void _exposure_set_white(dt_iop_module_t *self,
const float white)
{
dt_iop_exposure_params_t *p = self->params;
const float exposure = _required_exposure_slider_ev(self, p, white2exposure(white));
if(p->exposure == exposure) return;
p->exposure = exposure;
if(p->black >= white) _exposure_set_black(self, white - 0.01);
dt_iop_exposure_gui_data_t *g = self->gui_data;
DT_ENTER_GUI_UPDATE();
dt_bauhaus_slider_set(g->exposure, p->exposure);
DT_LEAVE_GUI_UPDATE();
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
static float _exposure_proxy_get_exposure(dt_iop_module_t *self)
{
dt_iop_exposure_params_t *p = self->params;
if(p->mode == EXPOSURE_MODE_DEFLICKER)
{
return p->deflicker_target_level;
}
else
{
return p->exposure;
}
}
static void _exposure_set_black(dt_iop_module_t *self,
const float black)
{
dt_iop_exposure_params_t *p = self->params;
if(p->black == black) return;
p->black = black;
if(p->black >= exposure2white(_total_adjustment_ev(self, p)))
{
_exposure_set_white(self, p->black + 0.01);
}
dt_iop_exposure_gui_data_t *g = self->gui_data;
DT_ENTER_GUI_UPDATE();
dt_bauhaus_slider_set(g->black, p->black);
DT_LEAVE_GUI_UPDATE();
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
static float _exposure_proxy_get_black(dt_iop_module_t *self)
{
dt_iop_exposure_params_t *p = self->params;
return p->black;
}
// The exposure that is actually applied, for other modules to read through
// dev->proxy.exposure. Derived from the parameters on the caller's thread, like
// _exposure_proxy_get_exposure() and _exposure_proxy_get_black() above: a value cached
// by the pipe would be one commit behind whenever the caller asks right after a change.
// Reading params this way is only safe on the GTK thread, which owns them - see the
// thread contract on dt_dev_proxy_exposure_t in develop.h.
static float _exposure_proxy_get_effective_exposure(dt_iop_module_t *self)
{
const dt_iop_exposure_params_t *const p = self->params;
if(p->mode == EXPOSURE_MODE_DEFLICKER)
{
// deflicker computes its correction from the raw histogram inside the pipe, so the
// value it caches is the only source; it stays undefined until the preview pipe has
// run once, and there is no GUI to cache it in outside the darkroom
const dt_iop_exposure_gui_data_t *const g = self->gui_data;
if(!g) return 0.f;
dt_iop_gui_enter_critical_section(self);
const float computed = g->deflicker_computed_exposure;
dt_iop_gui_leave_critical_section(self);
return computed == EXPOSURE_CORRECTION_UNDEFINED ? 0.f : computed;
}
return _total_adjustment_ev(self, p);
}
static void _exposure_proxy_handle_event(int n_press,
gdouble delta,
GdkModifierType state,
const gboolean is_blackpoint)
{
const dt_iop_module_t *const self = darktable.develop->proxy.exposure.module;
if(self && self->gui_data)
{
const dt_iop_exposure_params_t *const p = self->params;
const dt_iop_exposure_gui_data_t *const g = self->gui_data;
GtkWidget *const widget =
is_blackpoint ? g->black : (p->mode == EXPOSURE_MODE_DEFLICKER
? g->deflicker_target_level : g->exposure);
const float val = dt_bauhaus_slider_get(widget);
const float accel = dt_accel_get_speed_multiplier(widget, state);
// delta > 0 means 'brighten'; increase exposure or move the black point correction
// in the negative direction
if(is_blackpoint)
delta = -delta;
if(n_press == 2)
dt_bauhaus_widget_reset(widget);
else if(!n_press)
{ // scroll
const float step = dt_bauhaus_slider_get_step(widget);
dt_bauhaus_slider_set(widget, val + delta * step * accel);
}
else
{ // drag
const float s_min = dt_bauhaus_slider_get_soft_min(widget);
const float s_max = dt_bauhaus_slider_get_soft_max(widget);
dt_bauhaus_slider_set(widget, val + delta * (s_max - s_min) * accel);
}
gchar *text = dt_bauhaus_slider_get_text(widget, dt_bauhaus_slider_get(widget));
dt_action_widget_toast(DT_ACTION(self), widget, "%s", text);
g_free(text);
}
}
static void _auto_set_exposure(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe)
{
dt_iop_exposure_gui_data_t *g = self->gui_data;
dt_iop_exposure_params_t *p = self->params;
// capture gui color picked event.
if(self->picked_color_max[0] < self->picked_color_min[0]) return;
const float *RGB = self->picked_color;
// Get input profile, assuming we are before colorin
const dt_iop_order_iccprofile_info_t *const input_profile =
dt_ioppr_get_pipe_input_profile_info(pipe);
if(input_profile == NULL) return;
// Convert to XYZ
dt_aligned_pixel_t XYZ;
dt_aligned_pixel_t Lab;
dot_product(RGB, input_profile->matrix_in, XYZ);
dt_XYZ_to_Lab(XYZ, Lab);
Lab[1] = Lab[2] = Lab[3] = 0.f; // make color gray to get only the equivalent lightness
dt_Lab_to_XYZ(Lab, XYZ);
dt_XYZ_to_sRGB(XYZ, g->spot_RGB);
// Convert to Lch for GUI feedback (input)
dt_aligned_pixel_t Lch;
dt_Lab_2_LCH(Lab, Lch);
// Write report in GUI
gchar *str = g_strdup_printf(_("L : \t%.1f %%"), Lch[0]);
DT_ENTER_GUI_UPDATE();
gtk_label_set_text(GTK_LABEL(g->Lch_origin), str);
DT_LEAVE_GUI_UPDATE();
g_free(str);
const dt_spot_mode_t mode = dt_bauhaus_combobox_get(g->spot_mode);
if(mode == DT_SPOT_MODE_MEASURE)
{
// the exposure the pipe applies, i.e. the user setting plus the compensations
const float exposure_adjustment = _total_adjustment_ev(self, p);
const float gain = exp2f(exposure_adjustment);
// apply the exposure compensation
dt_aligned_pixel_t XYZ_out = {0.0f };
for(int c = 0; c < 3; c++)
XYZ_out[c] = XYZ[c] * gain;
// Convert to Lab for GUI feedback
dt_aligned_pixel_t Lab_out;
dt_XYZ_to_Lab(XYZ_out, Lab_out);
Lab_out[1] = Lab_out[2] = 0.f; // make it gray
// Return the values in sliders
DT_ENTER_GUI_UPDATE();
dt_bauhaus_slider_set(g->lightness_spot, Lab_out[0]);
_paint_hue(self);
DT_LEAVE_GUI_UPDATE();
dt_conf_set_float("darkroom/modules/exposure/lightness", Lab_out[0]);
}
else if(mode == DT_SPOT_MODE_CORRECT)
{
// Get the target color in XYZ space
dt_aligned_pixel_t Lch_target = { 0.f };
dt_iop_gui_enter_critical_section(self);
Lch_target[0] = dt_bauhaus_slider_get(g->lightness_spot);
dt_iop_gui_leave_critical_section(self);
dt_aligned_pixel_t Lab_target = { 0.f };
dt_LCH_2_Lab(Lch_target, Lab_target);
dt_aligned_pixel_t XYZ_target = { 0.f };
dt_Lab_to_XYZ(Lab_target, XYZ_target);
// a near-black target cannot be matched, and a near-black sample explodes the ratio
if(XYZ[1] > 1e-5f && XYZ_target[1] > 1e-5f)
_exposure_set_white(self, XYZ[1] / XYZ_target[1]);
}
}
void color_picker_apply(dt_iop_module_t *self,
GtkWidget *picker,
dt_dev_pixelpipe_t *pipe)
{
DT_GUARD_GUI_UPDATE();
_auto_set_exposure(self, pipe);
}
// keep black under the white point the pipe will map to 1.0, so that
// white - black cannot reach zero or turn negative; the comparison is in the
// compensated domain because that is what the pipe uses, while p->black is
// never compensated
static void _clamp_black_below_white(dt_iop_module_t *self)
{
const dt_iop_exposure_params_t *const p = self->params;
const float white = exposure2white(_total_adjustment_ev(self, p));
if(p->black >= white)
_exposure_set_black(self, white - 0.01);
}
void gui_changed(dt_iop_module_t *self,
GtkWidget *w,
void *previous)
{
dt_iop_exposure_gui_data_t *g = self->gui_data;
dt_iop_exposure_params_t *p = self->params;
if(w == g->mode)
{
switch(p->mode)
{
case EXPOSURE_MODE_DEFLICKER:
_autoexp_disable(self);
if(!dt_image_is_raw(&self->dev->image_storage)
|| self->dev->image_storage.buf_dsc.channels != 1
|| self->dev->image_storage.buf_dsc.datatype != TYPE_UINT16)
{
// force manual for unsupported image type
p->mode = EXPOSURE_MODE_MANUAL;
dt_bauhaus_combobox_set(g->mode, p->mode);
gtk_widget_set_sensitive(GTK_WIDGET(g->mode), FALSE);
break;
}
gtk_stack_set_visible_child_name(GTK_STACK(g->mode_stack), "deflicker");
break;
case EXPOSURE_MODE_MANUAL:
default:
gtk_stack_set_visible_child_name(GTK_STACK(g->mode_stack), "manual");
break;
}
}
// black and the white point must not cross. to ensure that, adjust black if the user
// changed exposure controls, and adjust exposure if they moved black. only in manual
// mode: the manual white point is not applied in deflicker mode, but its hidden
// controls stay reachable via shortcuts, so manual white must not constrain the visible
// and active black
if(p->mode == EXPOSURE_MODE_MANUAL)
{
if(w == g->black)
{
const float white = exposure2white(_total_adjustment_ev(self, p));
if(p->black >= white)
_exposure_set_white(self, p->black + 0.01);
}
else if(w == g->mode
|| w == g->exposure
|| w == g->compensate_exposure_bias
|| w == g->compensate_hilite_preserv)
{
// the white point moved, or manual mode was just entered (normally, or
// forced for an unsupported image) with a black left over from deflicker
// mode's white point
_clamp_black_below_white(self);
}
}
}
static gboolean _show_computed(gpointer user_data)
{
dt_iop_module_t *self = user_data;
dt_iop_exposure_gui_data_t *g = self->gui_data;
dt_iop_gui_enter_critical_section(self);
if(g->deflicker_computed_exposure != EXPOSURE_CORRECTION_UNDEFINED)
{
gchar *str = g_strdup_printf(_("%.2f EV"), g->deflicker_computed_exposure);
gtk_label_set_text(g->deflicker_used_EC, str);
g_free(str);
}
dt_iop_gui_leave_critical_section(self);
return G_SOURCE_REMOVE;
}
static gboolean _target_color_draw(GtkWidget *widget,
cairo_t *crf,
const dt_iop_module_t *self)
{
dt_iop_exposure_gui_data_t *g = self->gui_data;
// Init
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
int width = allocation.width, height = allocation.height;
cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
cairo_t *cr = cairo_create(cst);
// Margins
const double INNER_PADDING = 4.0;
const float margin = 2. * DT_PIXEL_APPLY_DPI(1.5);
width -= 2* INNER_PADDING;
height -= 2 * margin;
// Paint target color
dt_aligned_pixel_t RGB = { 0 };
dt_aligned_pixel_t Lch = { 0 };
dt_aligned_pixel_t Lab = { 0 };
dt_aligned_pixel_t XYZ = { 0 };
Lch[0] = dt_bauhaus_slider_get(g->lightness_spot);
Lch[1] = 0.f;
Lch[2] = 0.f;
dt_LCH_2_Lab(Lch, Lab);
dt_Lab_to_XYZ(Lab, XYZ);
dt_XYZ_to_sRGB(XYZ, RGB);
cairo_set_source_rgb(cr, RGB[0], RGB[1], RGB[2]);
cairo_rectangle(cr, INNER_PADDING, margin, width, height);
cairo_fill(cr);
// Clean
cairo_stroke(cr);
cairo_destroy(cr);
cairo_set_source_surface(crf, cst, 0, 0);
cairo_paint(crf);
cairo_surface_destroy(cst);
return TRUE;
}
static gboolean _origin_color_draw(GtkWidget *widget,
cairo_t *crf,
dt_iop_module_t *self)
{
dt_iop_exposure_gui_data_t *g = self->gui_data;
// Init
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
int width = allocation.width;
int height = allocation.height;
cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
cairo_t *cr = cairo_create(cst);
// Margins
const double INNER_PADDING = 4.0;
const float margin = 2. * DT_PIXEL_APPLY_DPI(1.5);
width -= 2* INNER_PADDING;
height -= 2 * margin;
cairo_set_source_rgb(cr, g->spot_RGB[0], g->spot_RGB[1], g->spot_RGB[2]);
cairo_rectangle(cr, INNER_PADDING, margin, width, height);
cairo_fill(cr);
// Clean
cairo_stroke(cr);
cairo_destroy(cr);
cairo_set_source_surface(crf, cst, 0, 0);
cairo_paint(crf);
cairo_surface_destroy(cst);
return TRUE;
}
static void _paint_hue(dt_iop_module_t *self)
{
// update the fill background color of LCh sliders
dt_iop_exposure_gui_data_t *g = self->gui_data;
const float lightness_min = dt_bauhaus_slider_get_hard_min(g->lightness_spot);
const float lightness_max = dt_bauhaus_slider_get_hard_max(g->lightness_spot);
const float lightness_range = lightness_max - lightness_min;
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 = lightness_min + stop * lightness_range;
dt_aligned_pixel_t RGB = { 0 };
const dt_aligned_pixel_t Lch = { x, 0.f, 0. };
dt_aligned_pixel_t Lab = { 0 };
dt_aligned_pixel_t XYZ = { 0 };
dt_LCH_2_Lab(Lch, Lab);
dt_Lab_to_XYZ(Lab, XYZ);
dt_XYZ_to_sRGB(XYZ, RGB);
dt_bauhaus_slider_set_stop(g->lightness_spot, stop, RGB[0], RGB[1], RGB[2]);
}
gtk_widget_queue_draw(g->lightness_spot);
gtk_widget_queue_draw(g->target_spot);
}
static void _spot_settings_changed_callback(GtkWidget *slider,
dt_iop_module_t *self)
{
DT_TRY_GUI_UPDATE();
dt_iop_exposure_gui_data_t *g = self->gui_data;
dt_aligned_pixel_t Lch_target = { 0.f };
Lch_target[0] = dt_bauhaus_slider_get(g->lightness_spot);
// Save the color on change
dt_conf_set_float("darkroom/modules/exposure/lightness", Lch_target[0]);
_paint_hue(self);
// Re-run auto compute if color picker active and mode is correct
const dt_spot_mode_t mode = dt_bauhaus_combobox_get(g->spot_mode);
DT_LEAVE_GUI_UPDATE();
if(mode == DT_SPOT_MODE_CORRECT && dt_iop_color_picker_is_active(g->exposure))
_auto_set_exposure(self, darktable.develop->full.pipe);
// else : just record new values and do nothing
}
void gui_reset(dt_iop_module_t *self)
{
dt_iop_color_picker_reset(self, TRUE);
}
void gui_init(dt_iop_module_t *self)
{
dt_iop_exposure_gui_data_t *g = IOP_GUI_ALLOC(exposure);
g->mode_stack = GTK_STACK(gtk_stack_new());
gtk_stack_set_homogeneous(GTK_STACK(g->mode_stack),FALSE);
GtkWidget *vbox_manual = self->widget = dt_gui_vbox();
gtk_stack_add_named(GTK_STACK(g->mode_stack), vbox_manual, "manual");
g->compensate_exposure_bias = dt_bauhaus_toggle_from_params
(self, "compensate_exposure_bias");
// the label carries the bias read off the image, so shorten it in the
// middle rather than at the end, where that value sits
dt_bauhaus_widget_set_label_ellipsize(g->compensate_exposure_bias,
PANGO_ELLIPSIZE_MIDDLE);
gtk_widget_set_tooltip_text(g->compensate_exposure_bias,
_("automatically remove the camera exposure bias\n"
"this is useful if you exposed the image to the right."));
g->compensate_hilite_preserv = dt_bauhaus_toggle_from_params
(self, "compensate_hilite_pres");
dt_bauhaus_widget_set_label_ellipsize(g->compensate_hilite_preserv,
PANGO_ELLIPSIZE_MIDDLE);
gtk_widget_set_tooltip_text(g->compensate_hilite_preserv,
_("remove the camera's hidden exposure bias in\n"
"HDR / highlight preservation / dynamic range / HLG tone mode.\n"));
g->exposure = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,
dt_bauhaus_slider_from_params(self, N_("exposure")));
gtk_widget_set_tooltip_text(g->exposure, _("adjust the exposure correction"));
dt_bauhaus_slider_set_digits(g->exposure, 3);
dt_bauhaus_slider_set_format(g->exposure, _(" EV"));
dt_bauhaus_slider_set_soft_range(g->exposure, -3.0, 4.0);
dt_bauhaus_widget_set_quad_tooltip(g->exposure, _("set the exposure adjustment using the selected area"));
dt_shortcut_register(dt_action_widget(g->exposure), 0, 0, GDK_KEY_e, 0);
GtkWidget *vbox_deflicker = self->widget = dt_gui_vbox();
gtk_stack_add_named(GTK_STACK(g->mode_stack), vbox_deflicker, "deflicker");
g->deflicker_percentile = dt_bauhaus_slider_from_params(self, "deflicker_percentile");
dt_bauhaus_slider_set_format(g->deflicker_percentile, "%");
gtk_widget_set_tooltip_text
(g->deflicker_percentile,
// xgettext:no-c-format
_("where in the histogram to meter for deflicking. E.g. 50% is median"));
g->deflicker_target_level = dt_bauhaus_slider_from_params(self, "deflicker_target_level");
dt_bauhaus_slider_set_format(g->deflicker_target_level, _(" EV"));
gtk_widget_set_tooltip_text
(g->deflicker_target_level,
_("where to place the exposure level for processed pics, EV below overexposure."));
g->deflicker_used_EC = GTK_LABEL(dt_ui_label_new("")); // This gets filled in by process
gtk_widget_set_tooltip_text(GTK_WIDGET(g->deflicker_used_EC),
_("what exposure correction has actually been used"));
dt_iop_gui_enter_critical_section(self);
g->deflicker_computed_exposure = EXPOSURE_CORRECTION_UNDEFINED;
dt_iop_gui_leave_critical_section(self);
dt_gui_box_add(vbox_deflicker, dt_gui_hbox(dt_ui_label_new(_("computed EC: ")),
g->deflicker_used_EC));
// Start building top level widget
self->widget = dt_gui_vbox();
g->mode = dt_bauhaus_combobox_from_params(self, N_("mode"));
dt_gui_box_add(self->widget, g->mode_stack);
g->black = dt_bauhaus_slider_from_params(self, "black");
gtk_widget_set_tooltip_text
(g->black,
_("adjust the black level to unclip negative RGB values.\n"
"you should never use it to add more density in blacks!\n"
"if poorly set, it will clip near-black colors out of gamut\n"
"by pushing RGB values into negatives."));
dt_bauhaus_slider_set_digits(g->black, 4);
dt_bauhaus_slider_set_soft_range(g->black, -0.1, 0.1);
dt_gui_new_collapsible_section
(&g->cs,
"plugins/darkroom/exposure/mapping",
_("area exposure mapping"),
GTK_BOX(self->widget),
DT_ACTION(self));
gtk_widget_set_tooltip_text
(g->cs.expander,
_("define a target brightness, in terms of exposure,\n"
"for a selected region of the image (the control sample),\n"
"which you then match against the same target brightness\n"
"in other images. the control sample can either\n"
"be a critical part of your subject or a non-moving and\n"
"consistently-lit surface over your series of images."));
DT_BAUHAUS_COMBOBOX_NEW_FULL
(g->spot_mode, self, NULL, N_("area mode"),
_("\"correction\" automatically adjust exposure\n"
"such that the input lightness is mapped to the target.\n"
"\"measure\" simply shows how an input color is mapped by\n"
"the exposure compensation and can be used to define a target."),
0, _spot_settings_changed_callback, self,
N_("correction"),
N_("measure"));
g->origin_spot = GTK_WIDGET(gtk_drawing_area_new());
gtk_widget_set_vexpand(g->origin_spot, TRUE);
gtk_widget_set_size_request(g->origin_spot,
2 * DT_PIXEL_APPLY_DPI(darktable.bauhaus->quad_width),
DT_PIXEL_APPLY_DPI(darktable.bauhaus->quad_width));
gtk_widget_set_tooltip_text(GTK_WIDGET(g->origin_spot),
_("the input color that should be mapped to the target"));
g_signal_connect(G_OBJECT(g->origin_spot), "draw", G_CALLBACK(_origin_color_draw), self);
g->Lch_origin = gtk_label_new(_("L : \tN/A"));
gtk_widget_set_tooltip_text
(GTK_WIDGET(g->Lch_origin),
_("these LCh coordinates are computed from CIE Lab 1976 coordinates"));
g->target_spot = GTK_WIDGET(gtk_drawing_area_new());
gtk_widget_set_vexpand(g->target_spot, TRUE);
gtk_widget_set_size_request(g->target_spot,
2 * DT_PIXEL_APPLY_DPI(darktable.bauhaus->quad_width),
DT_PIXEL_APPLY_DPI(darktable.bauhaus->quad_width));
gtk_widget_set_tooltip_text(GTK_WIDGET(g->target_spot),
_("the desired target exposure after mapping"));
g_signal_connect(G_OBJECT(g->target_spot), "draw", G_CALLBACK(_target_color_draw), self);
g->lightness_spot = dt_bauhaus_slider_new_with_range(self, 0., 100., 0, 50.f, 1);
dt_bauhaus_widget_set_label(g->lightness_spot, NULL, N_("lightness"));
dt_bauhaus_slider_set_format(g->lightness_spot, "%");
g_signal_connect(G_OBJECT(g->lightness_spot), "value-changed",
G_CALLBACK(_spot_settings_changed_callback), self);
dt_gui_box_add(g->cs.container,
g->spot_mode,
dt_gui_hbox( dt_gui_vbox(dt_ui_section_label_new(C_("section", "input")),
g->origin_spot, g->Lch_origin),
gtk_label_new(" "), dt_gui_expand( // spacer
dt_gui_vbox(dt_ui_section_label_new(C_("section", "target")),
g->target_spot, g->lightness_spot))));
/* register hooks with current dev so that histogram
can interact with this module.
*/
dt_dev_proxy_exposure_t *instance = &darktable.develop->proxy.exposure;
instance->module = self;
instance->get_exposure = _exposure_proxy_get_exposure;
instance->get_effective_exposure = _exposure_proxy_get_effective_exposure;
instance->get_black = _exposure_proxy_get_black;
instance->handle_event = _exposure_proxy_handle_event;
}
void gui_cleanup(dt_iop_module_t *self)
{
if(darktable.develop->proxy.exposure.module == self)
darktable.develop->proxy.exposure.module = NULL;
g_idle_remove_by_data(self);
}
// 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