diff --git a/src/develop/develop.c b/src/develop/develop.c index 61b9cae2a7d7..12b83c9f373c 100644 --- a/src/develop/develop.c +++ b/src/develop/develop.c @@ -2951,6 +2951,43 @@ float dt_dev_exposure_get_exposure(dt_develop_t *dev) return instance && instance->get_exposure && instance->module->enabled ? instance->get_exposure(instance->module) : 0.0f; } +float dt_dev_exposure_get_effective_exposure(dt_develop_t *dev) +{ + if (dt_view_get_current() != DT_VIEW_DARKROOM) + { + return 0.0f; + } + + // The proxy function pointers are only set if an exposure module has been initialized. + if (!dev->proxy.exposure.get_effective_exposure) + { + return 0.0f; + } + + const dt_iop_module_so_t *exposure_so = NULL; + + for(const GList *modules = darktable.iop; modules; modules = g_list_next(modules)) + { + const dt_iop_module_so_t *module_so = modules->data; + if(dt_iop_module_is(module_so, "exposure")) + { + exposure_so = module_so; + break; + } + } + + if (exposure_so) + { + dt_iop_module_t *preferred_exposure_instance = dt_iop_get_module_enabled_preferring_unmasked_first_instance(exposure_so); + if (preferred_exposure_instance) + { + return dev->proxy.exposure.get_effective_exposure(preferred_exposure_instance); + } + } + + return 0.0f; +} + float dt_dev_exposure_get_black(dt_develop_t *dev) { const dt_dev_proxy_exposure_t *instance = _dev_exposure_proxy_available(dev); diff --git a/src/develop/develop.h b/src/develop/develop.h index 2f9fdca33d6b..ab2bfb45a59b 100644 --- a/src/develop/develop.h +++ b/src/develop/develop.h @@ -101,6 +101,7 @@ typedef struct dt_dev_proxy_exposure_t { struct dt_iop_module_t *module; float (*get_exposure)(struct dt_iop_module_t *exp); + float (*get_effective_exposure)(struct dt_iop_module_t *exp); float (*get_black)(struct dt_iop_module_t *exp); void (*handle_event)(gpointer, int, gdouble, const gboolean); } dt_dev_proxy_exposure_t; @@ -468,6 +469,8 @@ void dt_dev_configure(dt_dev_viewport_t *port); /** get exposure level */ float dt_dev_exposure_get_exposure(dt_develop_t *dev); +/** get final effective exposure level including compensations */ +float dt_dev_exposure_get_effective_exposure(dt_develop_t *dev); /** get exposure black level */ float dt_dev_exposure_get_black(dt_develop_t *dev); diff --git a/src/develop/imageop.c b/src/develop/imageop.c index 7a7c2f2d059d..33f49acfc94c 100644 --- a/src/develop/imageop.c +++ b/src/develop/imageop.c @@ -3636,9 +3636,16 @@ dt_iop_module_t *dt_iop_get_module_by_op_priority(GList *modules, return mod_ret; } +typedef enum instance_filter_mode_t +{ + FILTER_NEUTRAL, + FILTER_PREFER, + FILTER_REQUIRE, +} instance_filter_mode_t; + dt_iop_module_t *_find_preferred_instance(const dt_iop_module_so_t *module, const gboolean prefer_expanded, - const gboolean prefer_enabled, + const instance_filter_mode_t enabled_mode, const gboolean prefer_unmasked, const gboolean prefer_first) { @@ -3653,6 +3660,10 @@ dt_iop_module_t *_find_preferred_instance(const dt_iop_module_so_t *module, if(mod->so == module && mod->iop_order != INT_MAX) { + if (enabled_mode == FILTER_REQUIRE && !mod->enabled) continue; + + const gboolean prefer_enabled = enabled_mode == FILTER_PREFER; + const gboolean no_mask = mod->blend_params->mask_mode == DEVELOP_MASK_DISABLED || mod->blend_params->mask_mode == DEVELOP_MASK_ENABLED; const int score = (mod->expanded && prefer_expanded ? 8 : 0) @@ -3704,11 +3715,20 @@ dt_iop_module_t *dt_iop_get_module_preferred_instance(const dt_iop_module_so_t * else return _find_preferred_instance(module, dt_conf_get_bool("accel/prefer_expanded"), - dt_conf_get_bool("accel/prefer_enabled"), + dt_conf_get_bool("accel/prefer_enabled") ? FILTER_PREFER : FILTER_NEUTRAL, dt_conf_get_bool("accel/prefer_unmasked"), dt_conf_is_equal("accel/select_order", "first instance")); } +dt_iop_module_t *dt_iop_get_module_enabled_preferring_unmasked_first_instance(const dt_iop_module_so_t *module) +{ + return _find_preferred_instance(module, + FALSE, + FILTER_REQUIRE, + TRUE, + TRUE); +} + /** adds keyboard accels to the first module in the pipe to handle * where there are multiple instances */ void dt_iop_connect_accels_multi(dt_iop_module_so_t *module) @@ -3723,7 +3743,7 @@ void dt_iop_connect_accels_multi(dt_iop_module_so_t *module) if(!strcmp(module->op, "exposure")) darktable.develop->proxy.exposure.module = - _find_preferred_instance(module, FALSE, TRUE, TRUE, TRUE); + _find_preferred_instance(module, FALSE, FILTER_PREFER, TRUE, TRUE); } void dt_iop_connect_accels_all(void) diff --git a/src/develop/imageop.h b/src/develop/imageop.h index b153c4126d7f..740ef1edc772 100644 --- a/src/develop/imageop.h +++ b/src/develop/imageop.h @@ -457,6 +457,9 @@ int dt_iop_count_instances(dt_iop_module_so_t *module); /** return preferred module instance for shortcuts **/ dt_iop_module_t *dt_iop_get_module_preferred_instance(const dt_iop_module_so_t *module); +/** return an enabled instance, if any, preferring unmasked instances earlier in the pipe **/ +dt_iop_module_t *dt_iop_get_module_enabled_preferring_unmasked_first_instance(const dt_iop_module_so_t *module); + /** returns true if module is the first instance of this operation in the pipe */ gboolean dt_iop_is_first_instance(GList *modules, const dt_iop_module_t *module); diff --git a/src/iop/agx.c b/src/iop/agx.c index 56b5750dd003..8a71f44d6c80 100644 --- a/src/iop/agx.c +++ b/src/iop/agx.c @@ -19,6 +19,7 @@ #include "bauhaus/bauhaus.h" #include "common/colorspaces_inline_conversions.h" #include "common/custom_primaries.h" +#include "common/image.h" #include "common/iop_profile.h" #include "common/math.h" #include "common/matrices.h" @@ -37,7 +38,7 @@ #include #include -DT_MODULE_INTROSPECTION(6, dt_iop_agx_params_t) +DT_MODULE_INTROSPECTION(7, dt_iop_agx_params_t) const char *name() { @@ -79,6 +80,9 @@ dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, static const float _epsilon = 1E-6f; static const float _default_gamma = 2.2f; +static const int _red_index = 0; +static const int _green_index = 1; +static const int _blue_index = 2; typedef enum dt_iop_agx_base_primaries_t { @@ -100,13 +104,14 @@ typedef struct dt_iop_agx_params_t float look_original_hue_mix_ratio; // $MIN: 0.f $MAX: 1.f $DEFAULT: 0.f $DESCRIPTION: "preserve hue" // log mapping - float range_black_relative_exposure; // $MIN: -20.f $MAX: -0.1f $DEFAULT: -10.f $DESCRIPTION: "black relative exposure" - float range_white_relative_exposure; // $MIN: 0.1f $MAX: 20.f $DEFAULT: 6.5f $DESCRIPTION: "white relative exposure" - float dynamic_range_scaling; // $MIN: -0.5f $MAX: 2.f $DEFAULT: 0.1f $DESCRIPTION: "dynamic range scaling" + float range_black_relative_ev; // $MIN: -20.f $MAX: -0.1f $DEFAULT: -10.f $DESCRIPTION: "black relative exposure" + float range_white_relative_ev; // $MIN: 0.1f $MAX: 20.f $DEFAULT: 6.5f $DESCRIPTION: "white relative exposure" + float dynamic_range_scaling; // $MIN: -0.5f $MAX: 2.f $DEFAULT: 0.1f $DESCRIPTION: "dynamic range scaling" // curve params - comments indicate the original variables from https://www.desmos.com/calculator/yrysofmx8h - // Corresponds to p_x, but not directly -- allows shifting the default 0.18 towards black or white relative exposure - float curve_pivot_x_shift_ratio; // $MIN: -1.f $MAX: 1.f $DEFAULT: 0.f $DESCRIPTION: "pivot input shift" + // Corresponds to p_x; is displayed as EV using slider offset and scale. + // 0.606060606061f = 10/16.5, mid-gray's position if black relative exposure is -10 EV, white is +65. EV + float curve_pivot_x; // $MIN: 0.f $MAX: 1.f $DEFAULT: 0.606060606061f $DESCRIPTION: "pivot relative exposure" // Corresponds to p_y, but not directly -- needs application of gamma float curve_pivot_y_linear_output; // $MIN: 0.f $MAX: 1.f $DEFAULT: 0.18f $DESCRIPTION: "pivot target output" // P_slope @@ -138,7 +143,7 @@ typedef struct dt_iop_agx_params_t float master_outset_ratio; // $MIN: 0.f $MAX: 2.f $DEFAULT: 1.f $DESCRIPTION: "master purity boost" float master_unrotation_ratio; // $MIN: 0.f $MAX: 2.f $DEFAULT: 1.f $DESCRIPTION: "master rotation reversal" - float red_outset; // $MIN: 0.f $MAX: 2.f $DEFAULT: 0.f $DESCRIPTION: "red purity boost" + float red_outset; // $MIN: 0.f $MAX: 0.99f $DEFAULT: 0.f $DESCRIPTION: "red purity boost" float red_unrotation; // $MIN: -0.5236f $MAX: 0.5236f $DEFAULT: 0.f $DESCRIPTION: "red reverse rotation" float green_outset; // $MIN: 0.f $MAX: 0.99f $DEFAULT: 0.f $DESCRIPTION: "green purity boost" float green_unrotation; // $MIN: -0.5236f $MAX: 0.5236f $DEFAULT: 0.f $DESCRIPTION: "green reverse rotation" @@ -151,7 +156,7 @@ typedef struct dt_iop_agx_params_t typedef struct dt_iop_basic_curve_controls_t { - GtkWidget *curve_pivot_x_shift; + GtkWidget *curve_pivot_x; GtkWidget *curve_pivot_y_linear; GtkWidget *curve_contrast_around_pivot; GtkWidget *curve_toe_power; @@ -178,6 +183,8 @@ typedef struct dt_iop_agx_gui_data_t GtkWidget *black_exposure_picker; GtkWidget *white_exposure_picker; GtkWidget *security_factor; + GtkWidget *range_exposure_picker_group; + GtkWidget *btn_read_exposure; // basic curve controls for 'settings' and 'curve' page (if enabled) dt_iop_basic_curve_controls_t basic_curve_controls; @@ -196,8 +203,8 @@ typedef struct dt_iop_agx_gui_data_t typedef struct tone_mapping_params_t { - float min_ev; - float max_ev; + float black_relative_ev; + float white_relative_ev; float range_in_ev; float curve_gamma; @@ -359,6 +366,97 @@ int legacy_params(dt_iop_module_t *self, gboolean completely_reverse_primaries; // added in v5 } dt_iop_agx_params_v5_t; + typedef struct dt_iop_agx_params_v6_t + { + float look_lift; // replaces look_offset in v5->v6 + float look_slope; + float look_brightness; // replaces look_power in v5->v6 + float look_saturation; + float look_original_hue_mix_ratio; + + float range_black_relative_exposure; + float range_white_relative_exposure; + float dynamic_range_scaling; + + float curve_pivot_x_shift_ratio; + float curve_pivot_y_linear_output; + float curve_contrast_around_pivot; + float curve_linear_ratio_below_pivot; + float curve_linear_ratio_above_pivot; + float curve_toe_power; + float curve_shoulder_power; + float curve_gamma; + gboolean auto_gamma; + float curve_target_display_black_ratio; + float curve_target_display_white_ratio; + + dt_iop_agx_base_primaries_t base_primaries; + gboolean disable_primaries_adjustments; + float red_inset; + float red_rotation; + float green_inset; + float green_rotation; + float blue_inset; + float blue_rotation; + + float master_outset_ratio; + float master_unrotation_ratio; + float red_outset; + float red_unrotation; + float green_outset; + float green_unrotation; + float blue_outset; + float blue_unrotation; + + gboolean completely_reverse_primaries; // added in v5 + } dt_iop_agx_params_v6_t; + + + typedef struct dt_iop_agx_params_v7_t + { + float look_lift; + float look_slope; + float look_brightness; + float look_saturation; + float look_original_hue_mix_ratio; + + float range_black_relative_ev; + float range_white_relative_ev; + float dynamic_range_scaling; + + float curve_pivot_x; // replaces curve_pivot_x_shift_ratio in v6 -> v7 + float curve_pivot_y_linear_output; + float curve_contrast_around_pivot; + float curve_linear_ratio_below_pivot; + float curve_linear_ratio_above_pivot; + float curve_toe_power; + float curve_shoulder_power; + float curve_gamma; + gboolean auto_gamma; + float curve_target_display_black_ratio; + float curve_target_display_white_ratio; + + dt_iop_agx_base_primaries_t base_primaries; + gboolean disable_primaries_adjustments; + float red_inset; + float red_rotation; + float green_inset; + float green_rotation; + float blue_inset; + float blue_rotation; + + float master_outset_ratio; + float master_unrotation_ratio; + float red_outset; + float red_unrotation; + float green_outset; + float green_unrotation; + float blue_outset; + float blue_unrotation; + + gboolean completely_reverse_primaries; + } dt_iop_agx_params_v7_t; + if(old_version == 1) { typedef struct dt_iop_agx_params_v1_t @@ -434,6 +532,7 @@ int legacy_params(dt_iop_module_t *self, return 0; // success } + if(old_version == 2) { // v2 and v3 have the same layout, but some v2 parameters are converted to percentages. @@ -458,6 +557,7 @@ int legacy_params(dt_iop_module_t *self, return 0; // success } + if(old_version == 3) { // v3 and v4 have the same layout, so we convert in place, but now the GUI will take @@ -488,6 +588,7 @@ int legacy_params(dt_iop_module_t *self, return 0; // success } + if(old_version == 4) { // v5 adds 'completely_reverse_primaries' at the end of the struct @@ -499,16 +600,17 @@ int legacy_params(dt_iop_module_t *self, np->completely_reverse_primaries = FALSE; *new_params = np; - *new_params_size = sizeof(dt_iop_agx_params_t); + *new_params_size = sizeof(dt_iop_agx_params_v5_t); *new_version = 5; return 0; // success } + if(old_version == 5) { // v6 replaces look -> power with look -> brightness const dt_iop_agx_params_v5_t *op = old_params; - dt_iop_agx_params_t *np = calloc(1, sizeof(dt_iop_agx_params_t)); + dt_iop_agx_params_v6_t *np = calloc(1, sizeof(dt_iop_agx_params_v6_t)); // v5 and v6 are compatible at the binary level memcpy(np, op, sizeof(dt_iop_agx_params_v5_t)); @@ -518,12 +620,48 @@ int legacy_params(dt_iop_module_t *self, 1.f / (look_power_v5 * look_power_v5) : 1.f / fmaxf(look_power_v5, 0.01f); // max allowed brightness is 100 *new_params = np; - *new_params_size = sizeof(dt_iop_agx_params_t); + *new_params_size = sizeof(dt_iop_agx_params_v6_t); *new_version = 6; return 0; // success } + if(old_version == 6) + { + // v6 replaces look -> power with look -> brightness + const dt_iop_agx_params_v6_t *op = old_params; + dt_iop_agx_params_v7_t *np = calloc(1, sizeof(dt_iop_agx_params_v7_t)); + + // v6 and v7 are compatible at the binary level + memcpy(np, op, sizeof(dt_iop_agx_params_v6_t)); + + const float pivot_relative_ev = op->curve_pivot_x_shift_ratio > 0 ? + op->range_white_relative_exposure * op->curve_pivot_x_shift_ratio : + - (op->range_black_relative_exposure * op->curve_pivot_x_shift_ratio); + const float pivot_distance_in_ev_from_black = pivot_relative_ev - op->range_black_relative_exposure; + + if (pivot_distance_in_ev_from_black < _epsilon) + { + np->curve_pivot_x = _epsilon; + } + else + { + const float exposure_range_in_ev = op->range_white_relative_exposure - op->range_black_relative_exposure; + if (exposure_range_in_ev < _epsilon) + { + // should never happen, because of hard limits on the sliders + np->curve_pivot_x = 0.5f; + } + np->curve_pivot_x = CLIP(pivot_distance_in_ev_from_black / exposure_range_in_ev); + } + + *new_params = np; + *new_params_size = sizeof(dt_iop_agx_params_v7_t); + *new_version = 7; + + return 0; // success + } + return 1; // no other conversion possible } @@ -908,15 +1046,15 @@ static inline void _agx_look(dt_aligned_pixel_t pixel_in_out, static inline float _apply_log_encoding(const float x, const float range_in_ev, - const float min_ev) + const float black_relative_ev) { // Assume input is linear RGB relative to 0.18 mid-gray // Ensure value > 0 before log const float x_relative = fmaxf(_epsilon, x / 0.18f); - // normalise to [0, 1] based on min_ev and range_in_ev - const float mapped = (log2f(fmaxf(x_relative, 0.f)) - min_ev) / range_in_ev; + // normalise to [0, 1] based on black_relative_ev and range_in_ev + const float mapped = (log2f(fmaxf(x_relative, 0.f)) - black_relative_ev) / range_in_ev; // Clamp result to [0, 1] - this is the input domain for the curve - return CLAMPF(mapped, 0.f, 1.f); + return CLIP(mapped); } // see https://www.desmos.com/calculator/gijzff3wlv @@ -1012,28 +1150,8 @@ static inline float _calculate_pivot_y_at_gamma(const dt_iop_agx_params_t * p, static void _adjust_pivot(const dt_iop_agx_params_t *p, tone_mapping_params_t *tone_mapping_params) { - const float mid_gray_in_log_range = - fabsf(tone_mapping_params->min_ev / tone_mapping_params->range_in_ev); - - if(p->curve_pivot_x_shift_ratio < 0.f) - { - const float black_ratio = -p->curve_pivot_x_shift_ratio; - const float gray_ratio = 1.f - black_ratio; - tone_mapping_params->pivot_x = gray_ratio * mid_gray_in_log_range; - } - else if(p->curve_pivot_x_shift_ratio > 0.f) - { - const float white_ratio = p->curve_pivot_x_shift_ratio; - const float gray_ratio = 1.f - white_ratio; - tone_mapping_params->pivot_x = mid_gray_in_log_range * gray_ratio + white_ratio; - } - else - { - tone_mapping_params->pivot_x = mid_gray_in_log_range; - } - // don't allow pivot_x to touch the endpoints - tone_mapping_params->pivot_x = CLAMPF(tone_mapping_params->pivot_x, _epsilon, 1.f - _epsilon); + tone_mapping_params->pivot_x = CLAMPF(p->curve_pivot_x, _epsilon, 1.f - _epsilon); if(p->auto_gamma) { @@ -1053,9 +1171,9 @@ static void _adjust_pivot(const dt_iop_agx_params_t *p, static void _set_log_mapping_params(const dt_iop_agx_params_t *p, tone_mapping_params_t *curve_and_look_params) { - curve_and_look_params->max_ev = p->range_white_relative_exposure; - curve_and_look_params->min_ev = p->range_black_relative_exposure; - curve_and_look_params->range_in_ev = curve_and_look_params->max_ev - curve_and_look_params->min_ev; + curve_and_look_params->white_relative_ev = p->range_white_relative_ev; + curve_and_look_params->black_relative_ev = p->range_black_relative_ev; + curve_and_look_params->range_in_ev = curve_and_look_params->white_relative_ev - curve_and_look_params->black_relative_ev; } static inline float _calculate_slope_gamma_compensation(const float gamma, @@ -1251,6 +1369,48 @@ static primaries_params_t _get_primaries_params(const dt_iop_agx_params_t *p) return primaries_params; } +static void _update_pivot_x(const float old_black_ev, const float old_white_ev, dt_iop_module_t *self, dt_iop_agx_params_t *const p) +{ + const dt_iop_agx_gui_data_t *g = self->gui_data; + + const float new_black_ev = p->range_black_relative_ev; + const float new_white_ev = p->range_white_relative_ev; + const float new_range = new_white_ev - new_black_ev; + + const float old_pivot_x = p->curve_pivot_x; + const float old_range = old_white_ev - old_black_ev; + + // this is what we want to preserve + const float pivot_ev = old_black_ev + (old_pivot_x * old_range); + const float clamped_pivot_ev = CLAMPF(pivot_ev, new_black_ev, new_white_ev); + + // new_range is ensured to be > 0 due to hard limits on sliders + p->curve_pivot_x = (clamped_pivot_ev - new_black_ev) / new_range; + + darktable.gui->reset++; + GtkWidget* const slider = g->basic_curve_controls.curve_pivot_x; + dt_bauhaus_slider_set_factor(slider, new_range); + dt_bauhaus_slider_set_offset(slider, new_black_ev); + // 0 EV default with the new exposure params + dt_bauhaus_slider_set_default(slider, -new_black_ev / new_range); + dt_bauhaus_slider_set(slider, p->curve_pivot_x); + darktable.gui->reset--; +} + +static void _adjust_relative_exposure_from_exposure_params(dt_iop_module_t *self) +{ + dt_iop_agx_params_t *p = self->params; + + const float old_black_ev = p->range_black_relative_ev; + const float old_white_ev = p->range_white_relative_ev; + + const float exposure = dt_dev_exposure_get_effective_exposure(self->dev); + + p->range_black_relative_ev = -8.f + 0.5f * exposure; + p->range_white_relative_ev = 4.f + 0.8 * exposure; + _update_pivot_x(old_black_ev, old_white_ev, self, p); +} + static void _agx_tone_mapping(dt_aligned_pixel_t rgb_in_out, const tone_mapping_params_t *params, const dt_colormatrix_t rendering_to_xyz_transposed) @@ -1265,7 +1425,7 @@ static void _agx_tone_mapping(dt_aligned_pixel_t rgb_in_out, for_three_channels(k, aligned(rgb_in_out, transformed_pixel : 16)) { - const float log_value = _apply_log_encoding(rgb_in_out[k], params->range_in_ev, params->min_ev); + const float log_value = _apply_log_encoding(rgb_in_out[k], params->range_in_ev, params->black_relative_ev); transformed_pixel[k] = _apply_curve(log_value, params); } @@ -1299,129 +1459,123 @@ static void _agx_tone_mapping(dt_aligned_pixel_t rgb_in_out, static void _apply_auto_black_exposure(const dt_iop_module_t *self) { - if(darktable.gui->reset) return; - dt_iop_agx_params_t *p = self->params; const dt_iop_agx_gui_data_t *g = self->gui_data; const float black_norm = min3f(self->picked_color_min); - p->range_black_relative_exposure = + p->range_black_relative_ev = CLAMPF(log2f(fmaxf(_epsilon, black_norm) / 0.18f) * (1.f + p->dynamic_range_scaling), -20.f, -0.1f); ++darktable.gui->reset; - dt_bauhaus_slider_set(g->black_exposure_picker, p->range_black_relative_exposure); + dt_bauhaus_slider_set(g->black_exposure_picker, p->range_black_relative_ev); --darktable.gui->reset; } static void _apply_auto_white_exposure(const dt_iop_module_t *self) { - if(darktable.gui->reset) return; - dt_iop_agx_params_t *p = self->params; const dt_iop_agx_gui_data_t *g = self->gui_data; const float white_norm = max3f(self->picked_color_max); - p->range_white_relative_exposure = + p->range_white_relative_ev = CLAMPF(log2f(fmaxf(_epsilon, white_norm) / 0.18f) * (1.f + p->dynamic_range_scaling), 0.1f, 20.f); ++darktable.gui->reset; - dt_bauhaus_slider_set(g->white_exposure_picker, p->range_white_relative_exposure); + dt_bauhaus_slider_set(g->white_exposure_picker, p->range_white_relative_ev); --darktable.gui->reset; } static void _apply_auto_tune_exposure(const dt_iop_module_t *self) { - if(darktable.gui->reset) return; - dt_iop_agx_params_t *p = self->params; const dt_iop_agx_gui_data_t *g = self->gui_data; - // Black point const float black_norm = min3f(self->picked_color_min); - p->range_black_relative_exposure = + p->range_black_relative_ev = CLAMPF(log2f(fmaxf(_epsilon, black_norm) / 0.18f) * (1.f + p->dynamic_range_scaling), -20.f, -0.1f); - // White point const float white_norm = max3f(self->picked_color_max); - p->range_white_relative_exposure = + p->range_white_relative_ev = CLAMPF(log2f(fmaxf(_epsilon, white_norm) / 0.18f) * (1.f + p->dynamic_range_scaling), 0.1f, 20.f); ++darktable.gui->reset; - dt_bauhaus_slider_set(g->black_exposure_picker, p->range_black_relative_exposure); - dt_bauhaus_slider_set(g->white_exposure_picker, p->range_white_relative_exposure); + dt_bauhaus_slider_set(g->black_exposure_picker, p->range_black_relative_ev); + dt_bauhaus_slider_set(g->white_exposure_picker, p->range_white_relative_ev); --darktable.gui->reset; } -// Apply logic for pivot x picker -static void _apply_auto_pivot_x(dt_iop_module_t *self, const dt_iop_order_iccprofile_info_t *profile) +static void _read_exposure_params_callback(GtkWidget *widget, + dt_iop_module_t *self) { - if(darktable.gui->reset) return; + dt_iop_agx_gui_data_t *g = self->gui_data; + if (g) + { + _adjust_relative_exposure_from_exposure_params(self); + dt_iop_gui_update(self); + dt_dev_add_history_item(darktable.develop, self, TRUE); + } +} +// move only the pivot's relative (input) exposure, and recalculate its output based on mid-gray +static void _apply_auto_pivot_xy(dt_iop_module_t *self, const dt_iop_order_iccprofile_info_t *profile) +{ dt_iop_agx_params_t *p = self->params; const dt_iop_agx_gui_data_t *g = self->gui_data; // Calculate norm and EV of the picked color - const float norm = _luminance_from_profile(self->picked_color, profile); - const float picked_ev = log2f(fmaxf(_epsilon, norm) / 0.18f); - - // Calculate the target pivot_x based on the picked EV and the current EV range - const float min_ev = p->range_black_relative_exposure; - const float max_ev = p->range_white_relative_exposure; - const float range_in_ev = fmaxf(_epsilon, max_ev - min_ev); - const float target_pivot_x = CLAMPF((picked_ev - min_ev) / range_in_ev, 0.f, 1.f); - - // Calculate the required pivot_x_shift to achieve the target_pivot_x - const float base_pivot_x = fabsf(min_ev / range_in_ev); // Pivot representing 0 EV (mid-gray) - - dt_iop_agx_params_t params_with_mid_gray = *p; - params_with_mid_gray.curve_pivot_y_linear_output = 0.18f; - params_with_mid_gray.curve_pivot_x_shift_ratio = 0.f; + const float picked_input_luminance = _luminance_from_profile(self->picked_color, profile); + const float picked_ev = CLAMPF(log2f(fmaxf(_epsilon, picked_input_luminance) / 0.18f), + p->range_black_relative_ev, + p->range_white_relative_ev); + const float range = p->range_white_relative_ev - p->range_black_relative_ev; + const float picked_pivot_x = (picked_ev - p->range_black_relative_ev) / range; const tone_mapping_params_t tone_mapping_params = - _calculate_tone_mapping_params(¶ms_with_mid_gray); + _calculate_tone_mapping_params(p); - // see where the target_pivot would be mapped with defaults of mid-gray to mid-gray mapped - const float target_y = _apply_curve(target_pivot_x, &tone_mapping_params); + // see where the target_pivot is currently mapped + const float target_y = _apply_curve(picked_pivot_x, &tone_mapping_params); // try to avoid changing the brightness of the pivot - const float target_y_linearised = powf(target_y, tone_mapping_params.curve_gamma); + const float target_y_linearised = powf(target_y, p->curve_gamma); p->curve_pivot_y_linear_output = target_y_linearised; + p->curve_pivot_x = picked_pivot_x; - float shift; - if(fabsf(target_pivot_x - base_pivot_x) < _epsilon) - { - shift = 0.f; - } - else if(base_pivot_x > target_pivot_x) - { - // Solve target_pivot_x = (1 + s) * base_pivot_x for s - shift = (base_pivot_x > _epsilon) ? (target_pivot_x / base_pivot_x) - 1.f : -1.f; - } - else // target_pivot_x > base_pivot_x - { - // Solve target_pivot_x = base_pivot_x * (1 - s) + s for s - const float denominator = 1.f - base_pivot_x; - shift = (denominator > _epsilon) ? (target_pivot_x - base_pivot_x) / denominator : 1.f; - } - - p->curve_pivot_x_shift_ratio = CLAMPF(shift, -1.f, 1.f); - - // Update the slider visually ++darktable.gui->reset; - dt_bauhaus_slider_set(g->basic_curve_controls.curve_pivot_x_shift, - p->curve_pivot_x_shift_ratio); + dt_bauhaus_slider_set(g->basic_curve_controls.curve_pivot_x, + p->curve_pivot_x); dt_bauhaus_slider_set(g->basic_curve_controls.curve_pivot_y_linear, p->curve_pivot_y_linear_output); --darktable.gui->reset; } +// move only the pivot's relative (input) exposure, but don't change its output +static void _apply_auto_pivot_x(dt_iop_module_t *self, const dt_iop_order_iccprofile_info_t *profile) +{ + dt_iop_agx_params_t *p = self->params; + const dt_iop_agx_gui_data_t *g = self->gui_data; + + const float picked_input_luminance = _luminance_from_profile(self->picked_color, profile); + const float picked_ev = CLAMPF(log2f(fmaxf(_epsilon, picked_input_luminance) / 0.18f), + p->range_black_relative_ev, + p->range_white_relative_ev); + const float range = p->range_white_relative_ev - p->range_black_relative_ev; + + p->curve_pivot_x = (picked_ev - p->range_black_relative_ev) / range; + + // Update the slider visually + darktable.gui->reset++; + dt_bauhaus_slider_set(g->basic_curve_controls.curve_pivot_x, p->curve_pivot_x); + darktable.gui->reset--; +} + static void _create_matrices(const primaries_params_t *params, const dt_iop_order_iccprofile_info_t *pipe_work_profile, const dt_iop_order_iccprofile_info_t *base_profile, @@ -1583,7 +1737,7 @@ void process(dt_iop_module_t *self, for_each_channel(c) { const float component = pix_in[c]; - // allow about 22.5 EV above mid-grey, including out-of-gamut pixels, getting rid of NaNs + // allow about 22.5 EV above mid-gray, including out-of-gamut pixels, getting rid of NaNs sanitised_in[c] = isnan(component) ? 0.f : CLAMPF(component, -1e6f, 1e6f); } float *const restrict pix_out = out + k; @@ -1760,17 +1914,17 @@ static gboolean _agx_draw_curve(GtkWidget *widget, cairo_set_dash(cr, dashes, 2, 0); // Use the same dash pattern cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(0.5)); - const float min_ev = tone_mapping_params.min_ev; - const float max_ev = tone_mapping_params.max_ev; + const float black_relative_ev = tone_mapping_params.black_relative_ev; + const float white_relative_ev = tone_mapping_params.white_relative_ev; const float range_in_ev = tone_mapping_params.range_in_ev; if(range_in_ev > _epsilon) // avoid division by zero or tiny ranges { - for(int ev = ceilf(min_ev); ev <= floorf(max_ev); ++ev) + for(int ev = ceilf(black_relative_ev); ev <= floorf(white_relative_ev); ++ev) { - float x_norm = (ev - min_ev) / range_in_ev; + float x_norm = (ev - black_relative_ev) / range_in_ev; // stays within the graph bounds - x_norm = CLAMPF(x_norm, 0.f, 1.f); + x_norm = CLIP(x_norm); const float x_graph = x_norm * graph_width; cairo_move_to(cr, x_graph, 0); @@ -1778,7 +1932,7 @@ static gboolean _agx_draw_curve(GtkWidget *widget, cairo_stroke(cr); // label - if(ev % 5 == 0 || ev == ceilf(min_ev) || ev == floorf(max_ev)) + if(ev % 5 == 0 || ev == ceilf(black_relative_ev) || ev == floorf(white_relative_ev)) { cairo_save(cr); cairo_identity_matrix(cr); // reset transformations for text @@ -1942,20 +2096,40 @@ void gui_changed(dt_iop_module_t *self, GtkWidget *widget, void *previous) { - const dt_iop_agx_gui_data_t *g = self->gui_data; + dt_iop_agx_gui_data_t *g = self->gui_data; dt_iop_agx_params_t *p = self->params; + if(widget == g->black_exposure_picker) + { + const float old_black_ev = *(float*)previous; + const float old_white_ev = p->range_white_relative_ev; + + _update_pivot_x(old_black_ev, old_white_ev, self, p); + } + + if(widget == g->white_exposure_picker) + { + const float old_black_ev = p->range_black_relative_ev; + const float old_white_ev = *(float*)previous; + + _update_pivot_x(old_black_ev, old_white_ev, self, p); + } + if(widget == g->security_factor) { - darktable.gui->reset++; const float prev = *(float *)previous; const float ratio = (p->dynamic_range_scaling - prev) / (prev + 1.f); - p->range_black_relative_exposure *= (1.f + ratio); - p->range_white_relative_exposure *= (1.f + ratio); + const float old_black_ev = p->range_black_relative_ev; + const float old_white_ev = p->range_white_relative_ev; - dt_bauhaus_slider_set(g->black_exposure_picker, p->range_black_relative_exposure); - dt_bauhaus_slider_set(g->white_exposure_picker, p->range_white_relative_exposure); + p->range_black_relative_ev = old_black_ev * (1.f + ratio); + p->range_white_relative_ev = old_white_ev * (1.f + ratio); + _update_pivot_x(old_black_ev, old_white_ev, self, p); + + darktable.gui->reset++; + dt_bauhaus_slider_set(g->black_exposure_picker, p->range_black_relative_ev); + dt_bauhaus_slider_set(g->white_exposure_picker, p->range_white_relative_ev); darktable.gui->reset--; } @@ -1987,24 +2161,27 @@ static GtkWidget* _create_basic_curve_controls_box(dt_iop_module_t *self, dt_iop_module_t *section = DT_IOP_SECTION_FOR_PARAMS(self, NC_("section", "curve"), box); dt_iop_basic_curve_controls_t *controls = &g->basic_curve_controls; - // curve_pivot_x_shift with picker - slider = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, - dt_bauhaus_slider_from_params(section, "curve_pivot_x_shift_ratio")); - controls->curve_pivot_x_shift = slider; - dt_bauhaus_slider_set_format(slider, "%"); + // curve_pivot_x_relative_ev with picker + slider = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, dt_bauhaus_slider_from_params(section, "curve_pivot_x")); + controls->curve_pivot_x = slider; + dt_bauhaus_slider_set_format(slider, _(" EV")); dt_bauhaus_slider_set_digits(slider, 2); - dt_bauhaus_slider_set_factor(slider, 100.f); - dt_bauhaus_slider_set_soft_range(slider, -0.5f, 0.5f); - gtk_widget_set_tooltip_text(slider, _("shift the pivot input towards black(-) or white(+)")); + gtk_widget_set_tooltip_text(slider, _("set the pivot's input exposure in EV relative to mid-gray")); + dt_bauhaus_widget_set_quad_tooltip(slider, _("the average luminance of the selected region will be\n" + "used to set the pivot relative to mid-gray")); // curve_pivot_y_linear - slider = dt_bauhaus_slider_from_params(section, "curve_pivot_y_linear_output"); + slider = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, dt_bauhaus_slider_from_params(section, "curve_pivot_y_linear_output")); controls->curve_pivot_y_linear = slider; dt_bauhaus_slider_set_format(slider, "%"); dt_bauhaus_slider_set_digits(slider, 2); dt_bauhaus_slider_set_factor(slider, 100.f); dt_bauhaus_slider_set_soft_range(slider, 0.f, 1.f); gtk_widget_set_tooltip_text(slider, _("darken or brighten the pivot (linear output power)")); + dt_bauhaus_widget_set_quad_tooltip(slider, _("the average luminance of the selected region will be\n" + "used to set the pivot relative to mid-gray,\n" + "and the output will be adjusted based on the default\n" + "mid-gray to mid-gray mapping")); // curve_contrast_around_pivot slider = dt_bauhaus_slider_from_params(section, "curve_contrast_around_pivot"); @@ -2012,22 +2189,6 @@ static GtkWidget* _create_basic_curve_controls_box(dt_iop_module_t *self, dt_bauhaus_slider_set_soft_range(slider, 0.1f, 5.f); gtk_widget_set_tooltip_text(slider, _("slope of the linear section around the pivot")); - // curve_toe_power - slider = dt_bauhaus_slider_from_params(section, "curve_toe_power"); - controls->curve_toe_power = slider; - dt_bauhaus_slider_set_soft_range(slider, 1.f, 5.f); - gtk_widget_set_tooltip_text(slider, _("contrast in shadows\n" - "higher values keep the slope nearly constant for longer,\n" - "at the cost of a more sudden drop near black")); - dt_bauhaus_widget_set_quad_tooltip(slider, - _("the curve has lost its 'S' shape, toe power cannot be applied.\n" - "target black cannot be reached with the selected contrast and pivot position.\n" - "increase contrast, move the pivot lower (reduce the pivot target output or\n" - "curve y gamma), or increase the distance between the pivot and the left edge\n" - "(increase the pivot shift, move the black point farther from the pivot by raising\n" - "the relative black exposure or move the white point closer to the pivot\n" - "by decreasing relative white exposure).")); - // curve_shoulder_power slider = dt_bauhaus_slider_from_params(section, "curve_shoulder_power"); controls->curve_shoulder_power = slider; @@ -2044,6 +2205,22 @@ static GtkWidget* _create_basic_curve_controls_box(dt_iop_module_t *self, "increasing relative white exposure or move the black point closer to the pivot\n" "by lowering relative black exposure).")); + // curve_toe_power + slider = dt_bauhaus_slider_from_params(section, "curve_toe_power"); + controls->curve_toe_power = slider; + dt_bauhaus_slider_set_soft_range(slider, 1.f, 5.f); + gtk_widget_set_tooltip_text(slider, _("contrast in shadows\n" + "higher values keep the slope nearly constant for longer,\n" + "at the cost of a more sudden drop near black")); + dt_bauhaus_widget_set_quad_tooltip(slider, + _("the curve has lost its 'S' shape, toe power cannot be applied.\n" + "target black cannot be reached with the selected contrast and pivot position.\n" + "increase contrast, move the pivot lower (reduce the pivot target output or\n" + "curve y gamma), or increase the distance between the pivot and the left edge\n" + "(increase the pivot shift, move the black point farther from the pivot by raising\n" + "the relative black exposure or move the white point closer to the pivot\n" + "by decreasing relative white exposure).")); + return box; } @@ -2141,41 +2318,41 @@ static GtkWidget* _create_advanced_box(dt_iop_module_t *self, // Reuse the slider variable for all sliders GtkWidget *slider = NULL; - // Toe length - slider = dt_bauhaus_slider_from_params(section, "curve_linear_ratio_below_pivot"); + // Shoulder length + slider = dt_bauhaus_slider_from_params(section, "curve_linear_ratio_above_pivot"); dt_bauhaus_slider_set_soft_range(slider, 0.f, 1.f); dt_bauhaus_slider_set_format(slider, "%"); dt_bauhaus_slider_set_digits(slider, 2); dt_bauhaus_slider_set_factor(slider, 100.f); gtk_widget_set_tooltip_text(slider, - _("length to keep curve linear below the pivot.\n" - "may crush shadows")); + _("length to keep curve linear above the pivot.\n" + "may clip highlights")); - // Toe intersection point - slider = dt_bauhaus_slider_from_params(section, "curve_target_display_black_ratio"); + // Shoulder intersection point + slider = dt_bauhaus_slider_from_params(section, "curve_target_display_white_ratio"); + dt_bauhaus_slider_set_soft_range(slider, 0.5f, 1.f); dt_bauhaus_slider_set_format(slider, "%"); dt_bauhaus_slider_set_digits(slider, 2); dt_bauhaus_slider_set_factor(slider, 100.f); - dt_bauhaus_slider_set_soft_range(slider, 0.f, 0.025f); - gtk_widget_set_tooltip_text(slider, _("raise for a faded look")); + gtk_widget_set_tooltip_text(slider, _("max linear output power")); - // Shoulder length - slider = dt_bauhaus_slider_from_params(section, "curve_linear_ratio_above_pivot"); + // Toe length + slider = dt_bauhaus_slider_from_params(section, "curve_linear_ratio_below_pivot"); dt_bauhaus_slider_set_soft_range(slider, 0.f, 1.f); dt_bauhaus_slider_set_format(slider, "%"); dt_bauhaus_slider_set_digits(slider, 2); dt_bauhaus_slider_set_factor(slider, 100.f); gtk_widget_set_tooltip_text(slider, - _("length to keep curve linear above the pivot.\n" - "may clip highlights")); + _("length to keep curve linear below the pivot.\n" + "may crush shadows")); - // Shoulder intersection point - slider = dt_bauhaus_slider_from_params(section, "curve_target_display_white_ratio"); - dt_bauhaus_slider_set_soft_range(slider, 0.5f, 1.f); + // Toe intersection point + slider = dt_bauhaus_slider_from_params(section, "curve_target_display_black_ratio"); dt_bauhaus_slider_set_format(slider, "%"); dt_bauhaus_slider_set_digits(slider, 2); dt_bauhaus_slider_set_factor(slider, 100.f); - gtk_widget_set_tooltip_text(slider, _("max linear output power")); + dt_bauhaus_slider_set_soft_range(slider, 0.f, 0.025f); + gtk_widget_set_tooltip_text(slider, _("raise for a faded look")); // curve_gamma g->auto_gamma = dt_bauhaus_toggle_from_params(section, "auto_gamma"); @@ -2199,26 +2376,27 @@ static GtkWidget* _create_advanced_box(dt_iop_module_t *self, return advanced_box; } -static void _add_exposure_box(dt_iop_module_t *self, dt_iop_agx_gui_data_t *g) +static void _add_exposure_box(dt_iop_module_t *self, dt_iop_agx_gui_data_t *g, dt_iop_module_t *real_self) { gchar *section_name = NC_("section", "input exposure range"); dt_gui_box_add(self->widget, dt_ui_section_label_new(Q_(section_name))); - g->white_exposure_picker = - dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, - dt_bauhaus_slider_from_params(self, "range_white_relative_exposure")); - dt_bauhaus_slider_set_soft_range(g->white_exposure_picker, 1.f, 20.f); + GtkWidget *white_slider = dt_bauhaus_slider_from_params(self, "range_white_relative_ev"); + g->white_exposure_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, white_slider); + dt_bauhaus_slider_set_soft_range(g->white_exposure_picker, 1.f, 10.f); dt_bauhaus_slider_set_format(g->white_exposure_picker, _(" EV")); gtk_widget_set_tooltip_text(g->white_exposure_picker, _("relative exposure above mid-gray (white point)")); + dt_bauhaus_widget_set_quad_tooltip(g->white_exposure_picker, _("pick the white point")); + - g->black_exposure_picker = - dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, - dt_bauhaus_slider_from_params(self, "range_black_relative_exposure")); + GtkWidget *black_slider = dt_bauhaus_slider_from_params(self, "range_black_relative_ev"); + g->black_exposure_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, black_slider); dt_bauhaus_slider_set_soft_range(g->black_exposure_picker, -20.f, -1.f); dt_bauhaus_slider_set_format(g->black_exposure_picker, _(" EV")); gtk_widget_set_tooltip_text(g->black_exposure_picker, _("relative exposure below mid-gray (black point)")); + dt_bauhaus_widget_set_quad_tooltip(g->black_exposure_picker, _("pick the black point")); g->security_factor = dt_bauhaus_slider_from_params(self, "dynamic_range_scaling"); dt_bauhaus_slider_set_soft_max(g->security_factor, 0.5f); @@ -2229,13 +2407,23 @@ static void _add_exposure_box(dt_iop_module_t *self, dt_iop_agx_gui_data_t *g) _("symmetrically increase or decrease the computed dynamic range.\n" "useful to give a safety margin to extreme luminances.")); - g->range_exposure_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, - dt_bauhaus_combobox_new(self)); - dt_bauhaus_widget_set_label(g->range_exposure_picker, NULL, N_("auto tune levels")); - gtk_widget_set_tooltip_text(g->range_exposure_picker, - _("pick image area to automatically set black and white exposure")); - dt_gui_box_add(self->widget, g->range_exposure_picker); + g->range_exposure_picker_group = dt_gui_hbox(); + + GtkWidget *auto_tune_box = dt_gui_hbox(); + GtkWidget *auto_tune_label = dt_ui_label_new(_("auto tune levels")); + g->range_exposure_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, NULL); + gtk_widget_set_tooltip_text(g->range_exposure_picker, _("set back and white relative exposure using the selected area")); + dt_action_define_iop(real_self, N_("exposure range"), N_("auto tune levels"), g->range_exposure_picker, &dt_action_def_toggle); + dt_gui_box_add(auto_tune_box, dt_gui_expand(auto_tune_label), g->range_exposure_picker); + dt_gui_box_add(g->range_exposure_picker_group, auto_tune_box); + + g->btn_read_exposure = dtgtk_button_new(dtgtk_cairo_paint_camera, 0, NULL); + gtk_widget_set_tooltip_text(g->btn_read_exposure, _("read exposure from metadata and exposure module")); + g_signal_connect(G_OBJECT(g->btn_read_exposure), "clicked", G_CALLBACK(_read_exposure_params_callback), real_self); + dt_action_define_iop(real_self, N_("exposure range"), N_("read exposure"), g->btn_read_exposure, &dt_action_def_button); + dt_gui_box_add(g->range_exposure_picker_group, g->btn_read_exposure); + dt_gui_box_add(self->widget, g->range_exposure_picker_group); } static void _apply_primaries_from_menu_callback(GtkMenuItem *menuitem, dt_iop_module_t *self) @@ -2293,12 +2481,110 @@ static void _set_post_curve_primaries_from_pre_callback(GtkWidget *widget, dt_io dt_dev_add_history_item(darktable.develop, self, TRUE); } -// GUI update (called when module UI is shown/refreshed) +typedef void (*hsv_updater_t)(dt_aligned_pixel_t hsv_out, float position_on_slider, float hue_deg, gboolean reverse_or_attenuate); + +static void _update_hsv_for_hue(dt_aligned_pixel_t hsv_out, const float position_on_slider, const float hue_deg, const gboolean reverse) +{ + const float hue_range_deg = 60.0f; + float hue_offset_deg = -hue_range_deg + position_on_slider * (2.0f * hue_range_deg); + if (reverse) hue_offset_deg = -hue_offset_deg; + + hsv_out[0] = fmodf(hue_deg + hue_offset_deg + 360.0f, 360.0f) / 360.0f; + hsv_out[1] = 0.7f; + hsv_out[2] = 1.0f; +} + +static void _update_hsv_for_purity(dt_aligned_pixel_t hsv_out, const float position_on_slider, const float hue_deg, const gboolean attenuate) +{ + hsv_out[0] = hue_deg / 360.0f; + hsv_out[1] = attenuate ? 1.0f - position_on_slider : position_on_slider; + hsv_out[2] = 1.0f; +} + +static void _paint_slider_gradient(GtkWidget *slider, const float hue_deg, const hsv_updater_t update_hsv, const gboolean attenuate_or_reverse) +{ + const float soft_min = dt_bauhaus_slider_get_soft_min(slider); + const float soft_max = dt_bauhaus_slider_get_soft_max(slider); + const float hard_min = dt_bauhaus_slider_get_hard_min(slider); + const float hard_max = dt_bauhaus_slider_get_hard_max(slider); + + dt_aligned_pixel_t hsv; + dt_aligned_pixel_t rgb; + + for (int stop = 0; stop < DT_BAUHAUS_SLIDER_MAX_STOPS; stop++) + { + const float position_on_slider = (float)stop / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1); + + // In order to have the desired, fixed visual clues, we must do some scaling, because bauhaus would + // paint according to the soft limits, so we rescale according to the hard ones. + const float value_in_soft_range = soft_min + position_on_slider * (soft_max - soft_min); + const float value_in_hard_range = (value_in_soft_range - hard_min) / (hard_max - hard_min); + + update_hsv(hsv, position_on_slider, hue_deg, attenuate_or_reverse); + + dt_HSV_2_RGB(hsv, rgb); + + dt_bauhaus_slider_set_stop(slider, value_in_hard_range, rgb[0], rgb[1], rgb[2]); + } + gtk_widget_queue_draw(GTK_WIDGET(slider)); +} + +static GtkWidget *_setup_purity_slider(dt_iop_module_t *self, + const char *param_name, + const char *tooltip, + const int primary_index, + const float hue_deg, + const gboolean attenuate) +{ + const float target_primary_value = 0.8f; + const float other_primaries_value = 0.2; + GtkWidget *slider = dt_bauhaus_slider_from_params(self, param_name); + dt_bauhaus_slider_set_feedback(slider, 0); + dt_bauhaus_slider_set_format(slider, "%"); + dt_bauhaus_slider_set_digits(slider, 2); + dt_bauhaus_slider_set_factor(slider, 100.f); + dt_bauhaus_slider_set_default(slider, 0.f); + + const float r = primary_index == _red_index ? target_primary_value : other_primaries_value; + const float g = primary_index == _green_index ? target_primary_value : other_primaries_value; + const float b = primary_index == _blue_index ? target_primary_value : other_primaries_value; + + dt_bauhaus_slider_set_stop(slider, 0.f, r, g, b); + gtk_widget_set_tooltip_text(slider, tooltip); + + _paint_slider_gradient(slider, hue_deg, &_update_hsv_for_purity, attenuate); + + return slider; +} + +static GtkWidget *_setup_hue_slider(dt_iop_module_t *self, + const char *param_name, + const char *tooltip, + const float hue_deg, + const gboolean reverse) +{ + GtkWidget *slider = dt_bauhaus_slider_from_params(self, param_name); + dt_bauhaus_slider_set_feedback(slider, 0); + dt_bauhaus_slider_set_format(slider, "°"); + dt_bauhaus_slider_set_digits(slider, 1); + dt_bauhaus_slider_set_factor(slider, RAD_2_DEG); + gtk_widget_set_tooltip_text(slider, tooltip); + dt_bauhaus_slider_set_default(slider, 0.f); + + _paint_slider_gradient(slider, hue_deg, &_update_hsv_for_hue, reverse); + + return slider; +} + void gui_update(dt_iop_module_t *self) { - const dt_iop_agx_gui_data_t *g = self->gui_data; + dt_iop_agx_gui_data_t *g = self->gui_data; const dt_iop_agx_params_t *p = self->params; + const float range = p->range_white_relative_ev - p->range_black_relative_ev; + dt_bauhaus_slider_set_factor(g->basic_curve_controls.curve_pivot_x, range); + dt_bauhaus_slider_set_offset(g->basic_curve_controls.curve_pivot_x, p->range_black_relative_ev); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->auto_gamma), p->auto_gamma); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->disable_primaries_adjustments), @@ -2306,6 +2592,7 @@ void gui_update(dt_iop_module_t *self) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->completely_reverse_primaries), p->completely_reverse_primaries); + gui_changed(self, NULL, NULL); } @@ -2347,35 +2634,49 @@ static void _create_primaries_page(dt_iop_module_t *main, dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "before tone mapping"))); GtkWidget *slider = NULL; - const float desaturation = 0.2f; -#define SETUP_COLOR_COMBO(color, r, g, b, attenuation_suffix, inset_tooltip, rotation_suffix, rotation_tooltip) \ - slider = dt_bauhaus_slider_from_params(self, #color attenuation_suffix); \ - dt_bauhaus_slider_set_format(slider, "%"); \ - dt_bauhaus_slider_set_digits(slider, 2); \ - dt_bauhaus_slider_set_factor(slider, 100.f); \ - dt_bauhaus_slider_set_soft_range(slider, 0.f, 0.5f); \ - dt_bauhaus_slider_set_stop(slider, 0.f, r, g, b); \ - gtk_widget_set_tooltip_text(slider, inset_tooltip); \ - \ - slider = dt_bauhaus_slider_from_params(self, #color rotation_suffix); \ - dt_bauhaus_slider_set_format(slider, "°"); \ - dt_bauhaus_slider_set_digits(slider, 1); \ - dt_bauhaus_slider_set_factor(slider, RAD_2_DEG); \ - dt_bauhaus_slider_set_stop(slider, 0.f, r, g, b); \ - gtk_widget_set_tooltip_text(slider, rotation_tooltip); - - SETUP_COLOR_COMBO(red, 1.f - desaturation, desaturation, desaturation, "_inset", - _("increase to desaturate reds in highlights faster"), - "_rotation", - _("shift the red primary towards yellow (+) or magenta (-)")); - SETUP_COLOR_COMBO(green, desaturation, 1.f - desaturation, desaturation, "_inset", - _("increase to desaturate greens in highlights faster"), - "_rotation", - _("shift the green primary towards cyan (+) or yellow (-)")); - SETUP_COLOR_COMBO(blue, desaturation, desaturation, 1.f - desaturation, "_inset", - _("increase to desaturate blues in highlights faster"), - "_rotation", - _("shift the blue primary towards magenta (+) or cyan (-)")); + + const float red_hue = 0.0f; + const float green_hue = 120.0f; + const float blue_hue = 240.0f; + + slider = _setup_purity_slider(self, + "red_inset", + _("increase to desaturate reds in highlights faster"), + _red_index, + red_hue, + TRUE); + + slider = _setup_hue_slider(self, + "red_rotation", + _("shift the red primary towards yellow (+) or magenta (-)"), + red_hue, + FALSE); + + slider = _setup_purity_slider(self, + "green_inset", + _("increase to desaturate greens in highlights faster"), + _green_index, + green_hue, + TRUE); + + slider = _setup_hue_slider(self, + "green_rotation", + _("shift the green primary towards cyan (+) or yellow (-)"), + green_hue, + FALSE); + + slider = _setup_purity_slider(self, + "blue_inset", + _("increase to desaturate blues in highlights faster"), + _blue_index, + blue_hue, + TRUE); + + slider = _setup_hue_slider(self, + "blue_rotation", + _("shift the blue primary towards magenta (+) or cyan (-)"), + blue_hue, + FALSE); GtkWidget *reversal_hbox = dt_gui_hbox(); g->post_curve_primaries_controls_vbox = dt_gui_vbox(); @@ -2400,27 +2701,56 @@ static void _create_primaries_page(dt_iop_module_t *main, dt_bauhaus_slider_set_format(slider, "%"); dt_bauhaus_slider_set_digits(slider, 2); dt_bauhaus_slider_set_factor(slider, 100.f); + // make sure a double-click sets it to 100%, overriding preset defaults + dt_bauhaus_slider_set_default(slider, 1.f); gtk_widget_set_tooltip_text(slider, _("overall purity boost")); slider = dt_bauhaus_slider_from_params(self, "master_unrotation_ratio"); dt_bauhaus_slider_set_format(slider, "%"); dt_bauhaus_slider_set_digits(slider, 2); dt_bauhaus_slider_set_factor(slider, 100.f); + // make sure a double-click sets it to 100%, overriding preset defaults + dt_bauhaus_slider_set_default(slider, 1.f); gtk_widget_set_tooltip_text(slider, _("overall unrotation ratio")); - SETUP_COLOR_COMBO(red, 1.f - desaturation, desaturation, desaturation, "_outset", - _("restore the purity of red, mostly in midtones and shadows"), - "_unrotation", - _("reverse the color shift in reds")); - SETUP_COLOR_COMBO(green, desaturation, 1.f - desaturation, desaturation, "_outset", - _("restore the purity of green, mostly in midtones and shadows"), - "_unrotation", - _("reverse the color shift in greens")); - SETUP_COLOR_COMBO(blue, desaturation, desaturation, 1.f - desaturation, "_outset", - _("restore the purity of blue, mostly in midtones and shadows"), - "_unrotation", - _("reverse the color shift in blues")); -#undef SETUP_COLOR_COMBO + slider = _setup_purity_slider(self, + "red_outset", + _("restore the purity of red, mostly in midtones and shadows"), + _red_index, + red_hue, + FALSE); + + slider = _setup_hue_slider(self, + "red_unrotation", + _("reverse the color shift in reds"), + red_hue, + TRUE); + + slider = _setup_purity_slider(self, + "green_outset", + _("restore the purity of green, mostly in midtones and shadows"), + _green_index, + green_hue, + FALSE); + + slider = _setup_hue_slider(self, + "green_unrotation", + _("reverse the color shift in greens"), + green_hue, + TRUE); + + slider = _setup_purity_slider(self, + "blue_outset", + _("restore the purity of blue, mostly in midtones and shadows"), + _blue_index, + blue_hue, + FALSE); + + slider = _setup_hue_slider(self, + "blue_unrotation", + _("reverse the color shift in blues"), + blue_hue, + TRUE); } static void _notebook_page_changed(GtkNotebook *notebook, @@ -2430,29 +2760,29 @@ static void _notebook_page_changed(GtkNotebook *notebook, { dt_iop_agx_gui_data_t *g = self->gui_data; GtkWidget *basics = g->curve_basic_controls_box; + GtkWidget *current_parent = gtk_widget_get_parent(basics); // 'settings' or 'curve' page only - if(page_num <= 1) + if(page_num <= 1 && current_parent) { - // prevent the widget from being destroyed when removed from its parent - g_object_ref(basics); - - // remove from the page it was on - gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(basics)), basics); + GtkWidget *target_container = (page_num == 0) ? gtk_widget_get_parent(g->range_exposure_picker_group) : page; - // pack to the now showing notebook page - dt_gui_box_add(page, basics); + if (current_parent != target_container) + { + g_object_ref(basics); + gtk_container_remove(GTK_CONTAINER(current_parent), basics); + dt_gui_box_add(target_container, basics); + g_object_unref(basics); + } int position = -1; if(page_num == 0) { - // on settings page, place after "auto tune levels" picker - gtk_container_child_get(GTK_CONTAINER(page), g->range_exposure_picker, - "position", &position, NULL); + // on settings page, place after "auto tune levels" picker group + gtk_container_child_get(GTK_CONTAINER(target_container), g->range_exposure_picker_group, + "position", &position, NULL); } - gtk_box_reorder_child(GTK_BOX(page), basics, ++position); - - g_object_unref(basics); + gtk_box_reorder_child(GTK_BOX(target_container), basics, ++position); } } @@ -2469,26 +2799,27 @@ void gui_init(dt_iop_module_t *self) g->curve_graph_box = _create_curve_graph_box(self, g); g->curve_advanced_controls_box = _create_advanced_box(self, g); - GtkWidget *current_page = dt_ui_notebook_page(g->notebook, + GtkWidget *settings_page = dt_ui_notebook_page(g->notebook, N_("settings"), _("main look and curve settings")); - dt_iop_module_t *settings_section = DT_IOP_SECTION_FOR_PARAMS(self, NULL, current_page); - _add_exposure_box(settings_section, g); + dt_iop_module_t *settings_section = DT_IOP_SECTION_FOR_PARAMS(self, NULL, settings_page); + _add_exposure_box(settings_section, g, self); dt_gui_box_add(settings_section->widget, g->curve_basic_controls_box); + GtkWidget *curve_page_parent = settings_page; if(dt_conf_get_bool("plugins/darkroom/agx/enable_curve_tab")) { - current_page = dt_ui_notebook_page(g->notebook, - N_("curve"), - _("detailed curve settings")); + curve_page_parent = dt_ui_notebook_page(g->notebook, + N_("curve"), + _("detailed curve settings")); // reparent on tab switch g_signal_connect(g->notebook, "switch-page", G_CALLBACK(_notebook_page_changed), self); } - dt_gui_box_add(current_page, g->curve_graph_box, - g->curve_advanced_controls_box); + dt_gui_box_add(curve_page_parent, g->curve_graph_box, + g->curve_advanced_controls_box); + // Finally, add the remaining sections to the settings page _add_look_box(settings_section, g); _create_primaries_page(self, g); - gui_update(self); } @@ -2503,8 +2834,8 @@ static void _set_shared_params(dt_iop_agx_params_t *p) // that we leave this as 0, based on feedback he had received p->look_original_hue_mix_ratio = 0.f; - p->range_black_relative_exposure = -10.f; - p->range_white_relative_exposure = 6.5f; + p->range_black_relative_ev = -10.f; + p->range_white_relative_ev = 6.5f; p->dynamic_range_scaling = 0.1f; p->curve_contrast_around_pivot = 2.8f; @@ -2516,7 +2847,7 @@ static void _set_shared_params(dt_iop_agx_params_t *p) p->curve_target_display_white_ratio = 1.f; p->auto_gamma = FALSE; p->curve_gamma = _default_gamma; - p->curve_pivot_x_shift_ratio = 0.f; + p->curve_pivot_x = -p->range_black_relative_ev / (p->range_white_relative_ev - p->range_black_relative_ev); p->curve_pivot_y_linear_output = 0.18f; } @@ -2635,17 +2966,22 @@ void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_t *pipe) { + if(darktable.gui->reset) return; + + dt_iop_agx_params_t *p = self->params; const dt_iop_agx_gui_data_t *g = self->gui_data; + const float old_black_ev = p->range_black_relative_ev; + const float old_white_ev = p->range_white_relative_ev; + if(picker == g->black_exposure_picker) _apply_auto_black_exposure(self); else if(picker == g->white_exposure_picker) _apply_auto_white_exposure(self); else if(picker == g->range_exposure_picker) _apply_auto_tune_exposure(self); - else if(picker == g->basic_curve_controls.curve_pivot_x_shift) - { - _apply_auto_pivot_x(self, dt_ioppr_get_pipe_work_profile_info(pipe)); - } + else if(picker == g->basic_curve_controls.curve_pivot_x) _apply_auto_pivot_x(self, dt_ioppr_get_pipe_work_profile_info(pipe)); + else if(picker == g->basic_curve_controls.curve_pivot_y_linear) _apply_auto_pivot_xy(self, dt_ioppr_get_pipe_work_profile_info(pipe)); + + _update_pivot_x(old_black_ev, old_white_ev, self, p); - const dt_iop_agx_params_t *p = self->params; if(p->auto_gamma) { ++darktable.gui->reset; @@ -2655,8 +2991,9 @@ void color_picker_apply(dt_iop_module_t *self, dt_bauhaus_slider_set(g->curve_gamma, tone_mapping_params.curve_gamma); --darktable.gui->reset; } - gtk_widget_queue_draw(GTK_WIDGET(g->graph_drawing_area)); + _update_curve_warnings(self); + gtk_widget_queue_draw(GTK_WIDGET(g->graph_drawing_area)); dt_dev_add_history_item(darktable.develop, self, TRUE); } diff --git a/src/iop/exposure.c b/src/iop/exposure.c index 59cc6d4d313d..ea67c6808326 100644 --- a/src/iop/exposure.c +++ b/src/iop/exposure.c @@ -87,6 +87,7 @@ typedef struct dt_iop_exposure_gui_data_t GtkLabel *deflicker_used_EC; GtkWidget *compensate_exposure_bias; GtkWidget *compensate_hilite_preserv; + volatile float effective_exposure; // used to cache the final computed exposure float deflicker_computed_exposure; GtkWidget *spot_mode; @@ -627,6 +628,11 @@ void commit_params(dt_iop_module_t *self, d->deflicker = 0; + if (self->gui_data) + { + ((dt_iop_exposure_gui_data_t *)self->gui_data)->effective_exposure = d->params.exposure; + } + if(p->mode == EXPOSURE_MODE_DEFLICKER && dt_image_is_raw(&self->dev->image_storage) && self->dev->image_storage.buf_dsc.channels == 1 @@ -813,6 +819,12 @@ static float _exposure_proxy_get_black(dt_iop_module_t *self) return p->black; } +static float _exposure_proxy_get_effective_exposure(dt_iop_module_t *self) +{ + dt_iop_exposure_gui_data_t *g = self->gui_data; + // should not be invoked when not in GUI mode + return g ? g->effective_exposure : 0.f; +} static void _exposure_proxy_handle_event(gpointer controller, int n_press, @@ -1320,6 +1332,7 @@ void gui_init(dt_iop_module_t *self) 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; }