/* This file is part of darktable, Copyright (C) 2019-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 "common/gdk_event_utils.h" #include "bauhaus/bauhaus.h" #include "common/iop_profile.h" #include "common/colorspaces_inline_conversions.h" #include "common/rgb_norms.h" #include "develop/imageop.h" #include "develop/imageop_math.h" #include "develop/imageop_gui.h" #include "dtgtk/drawingarea.h" #include "gui/color_picker_proxy.h" #include "gui/presets.h" #include "gui/accelerators.h" #include "libs/colorpicker.h" #define DT_GUI_CURVE_EDITOR_INSET DT_PIXEL_APPLY_DPI(1) #define DT_IOP_RGBCURVE_RES 256 #define DT_IOP_RGBCURVE_MAXNODES MAX_ANCHORS #define DT_IOP_RGBCURVE_MIN_X_DISTANCE 0.0025f // max iccprofile file name length // must be in synch with filename in dt_colorspaces_color_profile_t in colorspaces.h #define DT_IOP_COLOR_ICC_LEN 512 DT_MODULE_INTROSPECTION(1, dt_iop_rgbcurve_params_t) typedef enum rgbcurve_channel_t { DT_IOP_RGBCURVE_R = 0, DT_IOP_RGBCURVE_G = 1, DT_IOP_RGBCURVE_B = 2, DT_IOP_RGBCURVE_MAX_CHANNELS = 3 } rgbcurve_channel_t; typedef enum dt_iop_rgbcurve_autoscale_t { DT_S_SCALE_AUTOMATIC_RGB = 0, // $DESCRIPTION: "RGB, linked channels" DT_S_SCALE_MANUAL_RGB = 1 // $DESCRIPTION: "RGB, independent channels" } dt_iop_rgbcurve_autoscale_t; typedef struct dt_iop_rgbcurve_node_t { float x; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 float y; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 } dt_iop_rgbcurve_node_t; typedef struct dt_iop_rgbcurve_params_t { dt_iop_rgbcurve_node_t curve_nodes[DT_IOP_RGBCURVE_MAX_CHANNELS] [DT_IOP_RGBCURVE_MAXNODES]; // actual nodes for each curve int curve_num_nodes[DT_IOP_RGBCURVE_MAX_CHANNELS]; // $DEFAULT: 2 number of nodes per curve int curve_type[DT_IOP_RGBCURVE_MAX_CHANNELS]; // $DEFAULT: MONOTONE_HERMITE (CATMULL_ROM, MONOTONE_HERMITE, CUBIC_SPLINE) dt_iop_rgbcurve_autoscale_t curve_autoscale; // $DEFAULT: DT_S_SCALE_AUTOMATIC_RGB $DESCRIPTION: "mode" gboolean compensate_middle_grey; // $DEFAULT: 0 $DESCRIPTION: "compensate middle gray" scale the curve and histogram so middle gray is at .5 dt_iop_rgb_norms_t preserve_colors; // $DEFAULT: DT_RGB_NORM_LUMINANCE $DESCRIPTION: "preserve colors" } dt_iop_rgbcurve_params_t; typedef struct dt_iop_rgbcurve_gui_data_t { dt_draw_curve_t *minmax_curve[DT_IOP_RGBCURVE_MAX_CHANNELS]; // curves for gui to draw int minmax_curve_nodes[DT_IOP_RGBCURVE_MAX_CHANNELS]; int minmax_curve_type[DT_IOP_RGBCURVE_MAX_CHANNELS]; GtkBox *hbox; GtkDrawingArea *area; GtkWidget *autoscale; // (DT_S_SCALE_MANUAL_RGB, DT_S_SCALE_AUTOMATIC_RGB) GtkNotebook *channel_tabs; GtkWidget *colorpicker; GtkWidget *colorpicker_set_values; GtkWidget *interpolator; rgbcurve_channel_t channel; double mouse_x, mouse_y; int selected; float draw_ys[DT_IOP_RGBCURVE_RES]; float draw_min_ys[DT_IOP_RGBCURVE_RES]; float draw_max_ys[DT_IOP_RGBCURVE_RES]; GtkWidget *chk_compensate_middle_grey; GtkWidget *cmb_preserve_colors; float zoom_factor; float offset_x, offset_y; } dt_iop_rgbcurve_gui_data_t; typedef struct dt_iop_rgbcurve_data_t { float table[DT_IOP_RGBCURVE_MAX_CHANNELS][0x10000]; // precomputed look-up tables for tone curve dt_iop_rgbcurve_params_t params; dt_draw_curve_t *curve[DT_IOP_RGBCURVE_MAX_CHANNELS]; // curves for pipe piece and pixel processing float unbounded_coeffs[DT_IOP_RGBCURVE_MAX_CHANNELS][3]; // approximation for extrapolation gboolean curve_changed[DT_IOP_RGBCURVE_MAX_CHANNELS]; // curve type changed? dt_colorspaces_color_profile_type_t type_work; // working color profile char filename_work[DT_IOP_COLOR_ICC_LEN]; } dt_iop_rgbcurve_data_t; typedef float (*_curve_table_ptr)[0x10000]; typedef float (*_coeffs_table_ptr)[3]; typedef struct dt_iop_rgbcurve_global_data_t { int kernel_rgbcurve; } dt_iop_rgbcurve_global_data_t; const char *name() { return _("rgb curve"); } int default_group() { return IOP_GROUP_TONE | IOP_GROUP_GRADING; } int flags() { return IOP_FLAGS_SUPPORTS_BLENDING | IOP_FLAGS_ALLOW_TILING; } 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; } const char **description(dt_iop_module_t *self) { return dt_iop_set_description (self, _("alter an image’s tones using curves in RGB color space"), _("corrective and creative"), _("linear, RGB, display-referred"), _("non-linear, RGB"), _("linear, RGB, display-referred")); } void init_presets(dt_iop_module_so_t *self) { dt_iop_rgbcurve_params_t p; memset(&p, 0, sizeof(p)); p.curve_num_nodes[DT_IOP_RGBCURVE_R] = 6; p.curve_num_nodes[DT_IOP_RGBCURVE_G] = 7; p.curve_num_nodes[DT_IOP_RGBCURVE_B] = 7; p.curve_type[DT_IOP_RGBCURVE_R] = CUBIC_SPLINE; p.curve_type[DT_IOP_RGBCURVE_G] = CUBIC_SPLINE; p.curve_type[DT_IOP_RGBCURVE_B] = CUBIC_SPLINE; p.curve_autoscale = DT_S_SCALE_AUTOMATIC_RGB; p.compensate_middle_grey = TRUE; p.preserve_colors = 1; float linear_ab[7] = { 0.0, 0.08, 0.3, 0.5, 0.7, 0.92, 1.0 }; // linear a, b curves for presets for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_G][k].x = linear_ab[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_G][k].y = linear_ab[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_B][k].x = linear_ab[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_B][k].y = linear_ab[k]; // More useful low contrast curve (based on Samsung NX -2 Contrast) p.curve_nodes[DT_IOP_RGBCURVE_R][0].x = 0.000000; p.curve_nodes[DT_IOP_RGBCURVE_R][1].x = 0.003862; p.curve_nodes[DT_IOP_RGBCURVE_R][2].x = 0.076613; p.curve_nodes[DT_IOP_RGBCURVE_R][3].x = 0.169355; p.curve_nodes[DT_IOP_RGBCURVE_R][4].x = 0.774194; p.curve_nodes[DT_IOP_RGBCURVE_R][5].x = 1.000000; p.curve_nodes[DT_IOP_RGBCURVE_R][0].y = 0.000000; p.curve_nodes[DT_IOP_RGBCURVE_R][1].y = 0.007782; p.curve_nodes[DT_IOP_RGBCURVE_R][2].y = 0.156182; p.curve_nodes[DT_IOP_RGBCURVE_R][3].y = 0.290352; p.curve_nodes[DT_IOP_RGBCURVE_R][4].y = 0.773852; p.curve_nodes[DT_IOP_RGBCURVE_R][5].y = 1.000000; dt_gui_presets_add_generic(_("contrast | compression"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); p.curve_num_nodes[DT_IOP_RGBCURVE_R] = 7; float linear_L[7] = { 0.0, 0.08, 0.17, 0.50, 0.83, 0.92, 1.0 }; // Linear - no contrast for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].x = linear_L[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = linear_L[k]; dt_gui_presets_add_generic(_("linear (gamma 1.0)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // Linear contrast for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].x = linear_L[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = linear_L[k]; p.curve_nodes[DT_IOP_RGBCURVE_R][1].y -= 0.020; p.curve_nodes[DT_IOP_RGBCURVE_R][2].y -= 0.030; p.curve_nodes[DT_IOP_RGBCURVE_R][4].y += 0.030; p.curve_nodes[DT_IOP_RGBCURVE_R][5].y += 0.020; dt_gui_presets_add_generic(_("contrast | medium (linear)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].x = linear_L[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = linear_L[k]; p.curve_nodes[DT_IOP_RGBCURVE_R][1].y -= 0.040; p.curve_nodes[DT_IOP_RGBCURVE_R][2].y -= 0.060; p.curve_nodes[DT_IOP_RGBCURVE_R][4].y += 0.060; p.curve_nodes[DT_IOP_RGBCURVE_R][5].y += 0.040; dt_gui_presets_add_generic(_("contrast | high (linear)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // Gamma contrast for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].x = linear_L[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = linear_L[k]; p.curve_nodes[DT_IOP_RGBCURVE_R][1].y -= 0.020; p.curve_nodes[DT_IOP_RGBCURVE_R][2].y -= 0.030; p.curve_nodes[DT_IOP_RGBCURVE_R][4].y += 0.030; p.curve_nodes[DT_IOP_RGBCURVE_R][5].y += 0.020; for(int k = 1; k < 6; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].x = powf(p.curve_nodes[DT_IOP_RGBCURVE_R][k].x, 2.2f); for(int k = 1; k < 6; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = powf(p.curve_nodes[DT_IOP_RGBCURVE_R][k].y, 2.2f); dt_gui_presets_add_generic(_("contrast | medium (gamma 2.2)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].x = linear_L[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = linear_L[k]; p.curve_nodes[DT_IOP_RGBCURVE_R][1].y -= 0.040; p.curve_nodes[DT_IOP_RGBCURVE_R][2].y -= 0.060; p.curve_nodes[DT_IOP_RGBCURVE_R][4].y += 0.060; p.curve_nodes[DT_IOP_RGBCURVE_R][5].y += 0.040; for(int k = 1; k < 6; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].x = powf(p.curve_nodes[DT_IOP_RGBCURVE_R][k].x, 2.2f); for(int k = 1; k < 6; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = powf(p.curve_nodes[DT_IOP_RGBCURVE_R][k].y, 2.2f); dt_gui_presets_add_generic(_("contrast | high (gamma 2.2)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); /** for pure power-like functions, we need more nodes close to the bounds**/ p.curve_type[DT_IOP_RGBCURVE_R] = MONOTONE_HERMITE; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].x = linear_L[k]; for(int k = 0; k < 7; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = linear_L[k]; // Gamma 2.0 - no contrast for(int k = 1; k < 6; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = (linear_L[k] * linear_L[k]); dt_gui_presets_add_generic(_("non-contrast curve | gamma 2.0"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // Gamma 0.5 - no contrast for(int k = 1; k < 6; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = sqrtf(linear_L[k]); dt_gui_presets_add_generic(_("non-contrast curve | gamma 0.5"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // Log2 - no contrast for(int k = 1; k < 6; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = logf(linear_L[k] + 1.0f) / logf(2.0f); dt_gui_presets_add_generic(_("non-contrast curve | logarithm (base 2)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // Exp2 - no contrast for(int k = 1; k < 6; k++) p.curve_nodes[DT_IOP_RGBCURVE_R][k].y = powf(2.0f, linear_L[k]) - 1.0f; dt_gui_presets_add_generic(_("non-contrast curve | exponential (base 2)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); } static float _curve_to_mouse(const float x, const float zoom_factor, const float offset) { return (x - offset) * zoom_factor; } static float _mouse_to_curve(const float x, const float zoom_factor, const float offset) { return (x / zoom_factor) + offset; } static void picker_scale(const float *const in, float *out, dt_iop_rgbcurve_params_t *p, const dt_iop_order_iccprofile_info_t *const work_profile) { switch(p->curve_autoscale) { case DT_S_SCALE_MANUAL_RGB: if(p->compensate_middle_grey && work_profile) { for(int c = 0; c < 3; c++) out[c] = dt_ioppr_compensate_middle_grey(in[c], work_profile); } else { for(int c = 0; c < 3; c++) out[c] = in[c]; } break; case DT_S_SCALE_AUTOMATIC_RGB: { const float val = (work_profile) ? dt_ioppr_get_rgb_matrix_luminance(in, work_profile->matrix_in, work_profile->lut_in, work_profile->unbounded_coeffs_in, work_profile->lutsize, work_profile->nonlinearlut) : dt_camera_rgb_luminance(in); if(p->compensate_middle_grey && work_profile) { out[0] = dt_ioppr_compensate_middle_grey(val, work_profile); } else { out[0] = val; } out[1] = out[2] = 0.f; } break; } for(int c = 0; c < 3; c++) out[c] = CLIP(out[c]); } static void _rgbcurve_show_hide_controls(dt_iop_rgbcurve_params_t *p, dt_iop_rgbcurve_gui_data_t *g) { gtk_notebook_set_show_tabs(g->channel_tabs, p->curve_autoscale == DT_S_SCALE_MANUAL_RGB); gtk_widget_set_visible(g->cmb_preserve_colors, p->curve_autoscale == DT_S_SCALE_AUTOMATIC_RGB); } static inline int _rgbcurve_nodes(const int nodes) { // stored counts index the fixed node arrays, and the last node at nodes - 1 return CLAMP(nodes, 1, DT_IOP_RGBCURVE_MAXNODES); } static gboolean _is_identity(dt_iop_rgbcurve_params_t *p, rgbcurve_channel_t channel) { for(int k=0; kcurve_num_nodes[channel]; k++) if(p->curve_nodes[channel][k].x != p->curve_nodes[channel][k].y) return FALSE; return TRUE; } void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) { dt_iop_rgbcurve_gui_data_t *g = self->gui_data; dt_iop_rgbcurve_params_t *p = self->params; if(w == g->autoscale) { g->channel = DT_IOP_RGBCURVE_R; gtk_notebook_set_current_page(GTK_NOTEBOOK(g->channel_tabs), DT_IOP_RGBCURVE_R); _rgbcurve_show_hide_controls(p, g); // switching to manual scale, if G and B not touched yet, just // make them identical to global setting (R) if(p->curve_autoscale == DT_S_SCALE_MANUAL_RGB && _is_identity(p, DT_IOP_RGBCURVE_G) && _is_identity(p, DT_IOP_RGBCURVE_B)) { for(int k=0; kcurve_nodes[DT_IOP_RGBCURVE_G][k] = p->curve_nodes[DT_IOP_RGBCURVE_B][k] = p->curve_nodes[DT_IOP_RGBCURVE_R][k]; p->curve_num_nodes[DT_IOP_RGBCURVE_G] = p->curve_num_nodes[DT_IOP_RGBCURVE_B] = p->curve_num_nodes[DT_IOP_RGBCURVE_R]; p->curve_type[DT_IOP_RGBCURVE_G] = p->curve_type[DT_IOP_RGBCURVE_B] = p->curve_type[DT_IOP_RGBCURVE_R]; } } else if(w == g->chk_compensate_middle_grey) { const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_iop_work_profile_info(self, self->dev->iop); if(work_profile == NULL) return; for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) { for(int k = 0; k < p->curve_num_nodes[ch]; k++) { if(p->compensate_middle_grey) { // we transform the curve nodes from the image colorspace to lab p->curve_nodes[ch][k].x = dt_ioppr_compensate_middle_grey(p->curve_nodes[ch][k].x, work_profile); p->curve_nodes[ch][k].y = dt_ioppr_compensate_middle_grey(p->curve_nodes[ch][k].y, work_profile); } else { // we transform the curve nodes from lab to the image colorspace p->curve_nodes[ch][k].x = dt_ioppr_uncompensate_middle_grey(p->curve_nodes[ch][k].x, work_profile); p->curve_nodes[ch][k].y = dt_ioppr_uncompensate_middle_grey(p->curve_nodes[ch][k].y, work_profile); } } } self->histogram_middle_grey = p->compensate_middle_grey; } } static void interpolator_callback(GtkWidget *widget, dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_rgbcurve_params_t *p = self->params; dt_iop_rgbcurve_gui_data_t *g = self->gui_data; const int combo = dt_bauhaus_combobox_get(widget); if(combo == 0) p->curve_type[DT_IOP_RGBCURVE_R] = p->curve_type[DT_IOP_RGBCURVE_G] = p->curve_type[DT_IOP_RGBCURVE_B] = CUBIC_SPLINE; else if(combo == 1) p->curve_type[DT_IOP_RGBCURVE_R] = p->curve_type[DT_IOP_RGBCURVE_G] = p->curve_type[DT_IOP_RGBCURVE_B] = CATMULL_ROM; else if(combo == 2) p->curve_type[DT_IOP_RGBCURVE_R] = p->curve_type[DT_IOP_RGBCURVE_G] = p->curve_type[DT_IOP_RGBCURVE_B] = MONOTONE_HERMITE; dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } static void tab_switch_callback(GtkNotebook *notebook, GtkWidget *page, guint page_num, dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_rgbcurve_gui_data_t *g = self->gui_data; g->channel = (rgbcurve_channel_t)page_num; gtk_widget_queue_draw(GTK_WIDGET(g->area)); } static inline int _add_node(dt_iop_rgbcurve_node_t *curve_nodes, int *nodes, float x, float y) { int selected = -1; if(curve_nodes[0].x > x) selected = 0; else { for(int k = 1; k < *nodes; k++) { if(curve_nodes[k].x > x) { selected = k; break; } } } if(selected == -1) selected = *nodes; for(int i = *nodes; i > selected; i--) { curve_nodes[i].x = curve_nodes[i - 1].x; curve_nodes[i].y = curve_nodes[i - 1].y; } // found a new point curve_nodes[selected].x = x; curve_nodes[selected].y = y; (*nodes)++; return selected; } static inline int _add_node_from_picker (dt_iop_rgbcurve_params_t *p, const float *const in, const float increment, const int ch, const dt_iop_order_iccprofile_info_t *const work_profile) { float x = 0.f; float y = 0.f; float val = 0.f; if(p->curve_autoscale == DT_S_SCALE_AUTOMATIC_RGB) val = (work_profile) ? dt_ioppr_get_rgb_matrix_luminance(in, work_profile->matrix_in, work_profile->lut_in, work_profile->unbounded_coeffs_in, work_profile->lutsize, work_profile->nonlinearlut) : dt_camera_rgb_luminance(in); else val = in[ch]; if(p->compensate_middle_grey && work_profile) y = x = dt_ioppr_compensate_middle_grey(val, work_profile); else y = x = val; x = CLIP(x - increment); y = CLIP(y + increment); return _add_node(p->curve_nodes[ch], &p->curve_num_nodes[ch], x, y); } void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_t *pipe) { dt_iop_rgbcurve_gui_data_t *g = self->gui_data; if(picker == g->colorpicker_set_values) { dt_iop_rgbcurve_params_t *p = self->params; const dt_iop_rgbcurve_params_t *const d = self->default_params; const int ch = g->channel; const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(pipe); // reset current curve p->curve_num_nodes[ch] = d->curve_num_nodes[ch]; p->curve_type[ch] = d->curve_type[ch]; for(int k = 0; k < DT_IOP_RGBCURVE_MAXNODES; k++) { p->curve_nodes[ch][k].x = d->curve_nodes[ch][k].x; p->curve_nodes[ch][k].y = d->curve_nodes[ch][k].y; } const GdkModifierType state = dt_key_modifier_state(); int picker_set_upper_lower; // flat=0, lower=-1, upper=1 if(dt_modifier_is(state, GDK_CONTROL_MASK)) picker_set_upper_lower = 1; else if(dt_modifier_is(state, GDK_SHIFT_MASK)) picker_set_upper_lower = -1; else picker_set_upper_lower = 0; // now add 4 nodes: min, avg, center, max const float increment = 0.05f * picker_set_upper_lower; _add_node_from_picker(p, self->picked_color_min, 0.f, ch, work_profile); _add_node_from_picker(p, self->picked_color, increment, ch, work_profile); _add_node_from_picker(p, self->picked_color_max, 0.f, ch, work_profile); if(p->curve_num_nodes[ch] == 5) _add_node(p->curve_nodes[ch], &p->curve_num_nodes[ch], p->curve_nodes[ch][1].x - increment + (p->curve_nodes[ch][3].x - p->curve_nodes[ch][1].x) / 2.f, p->curve_nodes[ch][1].y + increment + (p->curve_nodes[ch][3].y - p->curve_nodes[ch][1].y) / 2.f); dt_dev_add_history_item(darktable.develop, self, TRUE); } dt_control_queue_redraw_widget(self->widget); } static gboolean _sanity_check(const float x, const int selected, const int nodes, const dt_iop_rgbcurve_node_t *curve) { gboolean point_valid = TRUE; // check if it is not too close to other node const float min_dist = DT_IOP_RGBCURVE_MIN_X_DISTANCE; // in curve coordinates if((selected > 0 && x - curve[selected - 1].x <= min_dist) || (selected < nodes - 1 && curve[selected + 1].x - x <= min_dist)) point_valid = FALSE; // for all points, x coordinate of point must be strictly larger than // the x coordinate of the previous point if((selected > 0 && (curve[selected - 1].x >= x)) || (selected < nodes - 1 && (curve[selected + 1].x <= x))) { point_valid = FALSE; } return point_valid; } static gboolean _move_point_internal(dt_iop_module_t *self, GtkWidget *widget, float dx, float dy, guint state) { dt_iop_rgbcurve_params_t *p = self->params; dt_iop_rgbcurve_gui_data_t *g = self->gui_data; const int ch = g->channel; dt_iop_rgbcurve_node_t *curve = p->curve_nodes[ch]; float multiplier = dt_accel_get_speed_multiplier(widget, state); dx *= multiplier; dy *= multiplier; const float new_x = CLIP(curve[g->selected].x + dx); const float new_y = CLIP(curve[g->selected].y + dy); gtk_widget_queue_draw(widget); if(_sanity_check(new_x, g->selected, p->curve_num_nodes[ch], p->curve_nodes[ch])) { curve[g->selected].x = new_x; curve[g->selected].y = new_y; dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); } return TRUE; } #define RGBCURVE_DEFAULT_STEP (0.001f) static void _area_scrolled_callback(GtkEventControllerScroll *controller, gdouble dx, gdouble dy, dt_iop_module_t *self) { GtkWidget *widget = dt_gui_get_widget(controller); dt_iop_rgbcurve_params_t *p = self->params; dt_iop_rgbcurve_gui_data_t *g = self->gui_data; if(darktable.develop->darkroom_skip_mouse_events) { if(dy != 0.0) { GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); const float mx = g->mouse_x; const float my = g->mouse_y; const float linx = _mouse_to_curve(mx, g->zoom_factor, g->offset_x), liny = _mouse_to_curve(my, g->zoom_factor, g->offset_y); g->zoom_factor *= 1.0 - 0.1 * dy; if(g->zoom_factor < 1.f) g->zoom_factor = 1.f; g->offset_x = linx - (mx / g->zoom_factor); g->offset_y = liny - (my / g->zoom_factor); g->offset_x = CLAMP(g->offset_x, 0.f, (g->zoom_factor - 1.f) / g->zoom_factor); g->offset_y = CLAMP(g->offset_y, 0.f, (g->zoom_factor - 1.f) / g->zoom_factor); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } } // if autoscale is on: do not modify g and b curves if((p->curve_autoscale != DT_S_SCALE_MANUAL_RGB) && g->channel != DT_IOP_RGBCURVE_R) if(g->selected < 0) return; dt_iop_color_picker_reset(self, TRUE); if(dy != 0.0) { dy *= -RGBCURVE_DEFAULT_STEP; _move_point_internal(self, widget, 0.0, dy, dt_key_modifier_state()); } } static gboolean _area_key_press_callback(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, dt_iop_module_t *self) { GtkWidget *widget = dt_gui_get_widget(controller); dt_iop_rgbcurve_params_t *p = self->params; dt_iop_rgbcurve_gui_data_t *g = self->gui_data; if(darktable.develop->darkroom_skip_mouse_events) return FALSE; // if autoscale is on: do not modify g and b curves if((p->curve_autoscale != DT_S_SCALE_MANUAL_RGB) && g->channel != DT_IOP_RGBCURVE_R) return TRUE; if(g->selected < 0) return FALSE; int handled = 0; float dx = 0.0f, dy = 0.0f; if(keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up) { handled = 1; dy = RGBCURVE_DEFAULT_STEP; } else if(keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down) { handled = 1; dy = -RGBCURVE_DEFAULT_STEP; } else if(keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right) { handled = 1; dx = RGBCURVE_DEFAULT_STEP; } else if(keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left) { handled = 1; dx = -RGBCURVE_DEFAULT_STEP; } if(!handled) return FALSE; dt_iop_color_picker_reset(self, TRUE); return _move_point_internal(self, widget, dx, dy, state); } #undef RGBCURVE_DEFAULT_STEP static void _area_leave_notify_callback(GtkEventControllerMotion *controller, dt_iop_module_t *self) { dt_iop_rgbcurve_gui_data_t *g = self->gui_data; if(!(dt_key_modifier_state() & GDK_BUTTON1_MASK)) g->selected = -1; gtk_widget_queue_draw(dt_gui_get_widget(controller)); } static gboolean _area_draw_callback(GtkWidget *widget, cairo_t *crf, dt_iop_module_t *self) { dt_iop_rgbcurve_gui_data_t *g = self->gui_data; dt_iop_rgbcurve_params_t *p = self->params; dt_develop_t *dev = darktable.develop; const int ch = g->channel; const int nodes = p->curve_num_nodes[ch]; const int autoscale = p->curve_autoscale; dt_iop_rgbcurve_node_t *curve_nodes = p->curve_nodes[ch]; if(g->minmax_curve_type[ch] != p->curve_type[ch] || g->minmax_curve_nodes[ch] != p->curve_num_nodes[ch]) { dt_draw_curve_destroy(g->minmax_curve[ch]); g->minmax_curve[ch] = dt_draw_curve_new(0.0, 1.0, p->curve_type[ch]); g->minmax_curve_nodes[ch] = p->curve_num_nodes[ch]; g->minmax_curve_type[ch] = p->curve_type[ch]; for(int k = 0; k < p->curve_num_nodes[ch]; k++) dt_draw_curve_add_point(g->minmax_curve[ch], p->curve_nodes[ch][k].x, p->curve_nodes[ch][k].y); } else { for(int k = 0; k < p->curve_num_nodes[ch]; k++) dt_draw_curve_set_point(g->minmax_curve[ch], k, p->curve_nodes[ch][k].x, p->curve_nodes[ch][k].y); } dt_draw_curve_t *minmax_curve = g->minmax_curve[ch]; dt_draw_curve_calc_values(minmax_curve, 0.0, 1.0, DT_IOP_RGBCURVE_RES, NULL, g->draw_ys); float unbounded_coeffs[3]; const float xm = curve_nodes[nodes - 1].x; { const float x[4] = { 0.7f * xm, 0.8f * xm, 0.9f * xm, 1.0f * xm }; const float y[4] = { g->draw_ys[CLAMP((int)(x[0] * DT_IOP_RGBCURVE_RES), 0, DT_IOP_RGBCURVE_RES - 1)], g->draw_ys[CLAMP((int)(x[1] * DT_IOP_RGBCURVE_RES), 0, DT_IOP_RGBCURVE_RES - 1)], g->draw_ys[CLAMP((int)(x[2] * DT_IOP_RGBCURVE_RES), 0, DT_IOP_RGBCURVE_RES - 1)], g->draw_ys[CLAMP((int)(x[3] * DT_IOP_RGBCURVE_RES), 0, DT_IOP_RGBCURVE_RES - 1)] }; dt_iop_estimate_exp(x, y, 4, unbounded_coeffs); } const int inset = DT_GUI_CURVE_EDITOR_INSET; 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); // clear bg cairo_set_source_rgb(cr, .2, .2, .2); cairo_paint(cr); cairo_translate(cr, inset, inset); width -= 2 * inset; height -= 2 * inset; char text[256]; cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.0)); cairo_set_source_rgb(cr, .1, .1, .1); cairo_rectangle(cr, 0, 0, width, height); cairo_stroke(cr); cairo_set_source_rgb(cr, .3, .3, .3); cairo_rectangle(cr, 0, 0, width, height); cairo_fill(cr); // draw grid cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(.4)); cairo_set_source_rgb(cr, .1, .1, .1); cairo_translate(cr, 0, height); dt_draw_grid_zoomed(cr, 4, 0.f, 0.f, 1.f, 1.f, width, height, g->zoom_factor, g->offset_x, g->offset_y); const double dashed[] = { 4.0, 4.0 }; const int len = sizeof(dashed) / sizeof(dashed[0]); cairo_set_dash(cr, dashed, len, 0); dt_draw_grid_zoomed(cr, 8, 0.f, 0.f, 1.f, 1.f, width, height, g->zoom_factor, g->offset_x, g->offset_y); cairo_set_dash(cr, dashed, 0, 0); // draw identity line cairo_move_to(cr, _curve_to_mouse(0.f, g->zoom_factor, g->offset_x) * width, _curve_to_mouse(0.f, g->zoom_factor, g->offset_y) * -height); cairo_line_to(cr, _curve_to_mouse(1.f, g->zoom_factor, g->offset_x) * width, _curve_to_mouse(1.f, g->zoom_factor, g->offset_y) * -height); cairo_stroke(cr); // if autoscale is on: do not display g and b curves if((autoscale != DT_S_SCALE_MANUAL_RGB) && ch != DT_IOP_RGBCURVE_R) goto finally; // draw nodes positions cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.)); cairo_set_source_rgb(cr, 0.6, 0.6, 0.6); for(int k = 0; k < nodes; k++) { const float x = _curve_to_mouse(curve_nodes[k].x, g->zoom_factor, g->offset_x), y = _curve_to_mouse(curve_nodes[k].y, g->zoom_factor, g->offset_y); cairo_arc(cr, x * width, -y * height, DT_PIXEL_APPLY_DPI(3), 0, 2. * M_PI); cairo_stroke(cr); } // draw selected cursor cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.)); if(g->selected >= 0) { cairo_set_source_rgb(cr, .9, .9, .9); const float x = _curve_to_mouse(curve_nodes[g->selected].x, g->zoom_factor, g->offset_x), y = _curve_to_mouse(curve_nodes[g->selected].y, g->zoom_factor, g->offset_y); cairo_arc(cr, x * width, -y * height, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI); cairo_stroke(cr); } // draw histogram in background // only if module is enabled if(self->enabled) { const uint32_t *hist = self->histogram; const gboolean is_linear = darktable.lib->proxy.histogram.is_linear; float hist_max; if(autoscale == DT_S_SCALE_AUTOMATIC_RGB) hist_max = max3ui(self->histogram_max); else hist_max = self->histogram_max[ch]; if(!is_linear) hist_max = logf(1.0 + hist_max); if(hist && hist_max > 0.0f) { cairo_push_group_with_content(cr, CAIRO_CONTENT_COLOR); cairo_scale(cr, width / 255.0, -(height - DT_PIXEL_APPLY_DPI(5)) / hist_max); if(autoscale == DT_S_SCALE_AUTOMATIC_RGB) { cairo_set_operator(cr, CAIRO_OPERATOR_ADD); for(int k=DT_IOP_RGBCURVE_R; kgraph_colors[k]); dt_draw_histogram_8_zoomed(cr, hist, 4, k, g->zoom_factor, g->offset_x * 255.0, g->offset_y * hist_max, is_linear); } } else if(autoscale == DT_S_SCALE_MANUAL_RGB) { set_color(cr, darktable.bauhaus->graph_colors[ch]); dt_draw_histogram_8_zoomed(cr, hist, 4, ch, g->zoom_factor, g->offset_x * 255.0, g->offset_y * hist_max, is_linear); } cairo_pop_group_to_source(cr); cairo_paint_with_alpha(cr, 0.2); } if(self->request_color_pick != DT_REQUEST_COLORPICK_OFF) { const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_iop_work_profile_info(self, self->dev->iop); dt_aligned_pixel_t picker_mean, picker_min, picker_max; // the global live samples ... GSList *samples = darktable.lib->proxy.colorpicker.live_samples; if(samples) { const dt_iop_order_iccprofile_info_t *const histogram_profile = dt_ioppr_get_histogram_profile_info(dev); if(work_profile && histogram_profile) { for(; samples; samples = g_slist_next(samples)) { dt_colorpicker_sample_t *sample = samples->data; // this functions need a 4c image for(int k = 0; k < 3; k++) { picker_mean[k] = sample->scope[DT_PICK_MEAN][k]; picker_min[k] = sample->scope[DT_PICK_MIN][k]; picker_max[k] = sample->scope[DT_PICK_MAX][k]; } picker_mean[3] = picker_min[3] = picker_max[3] = 1.f; dt_ioppr_transform_image_colorspace_rgb(picker_mean, picker_mean, 1, 1, histogram_profile, work_profile, "rgb curve"); dt_ioppr_transform_image_colorspace_rgb(picker_min, picker_min, 1, 1, histogram_profile, work_profile, "rgb curve"); dt_ioppr_transform_image_colorspace_rgb(picker_max, picker_max, 1, 1, histogram_profile, work_profile, "rgb curve"); picker_scale(picker_mean, picker_mean, p, work_profile); picker_scale(picker_min, picker_min, p, work_profile); picker_scale(picker_max, picker_max, p, work_profile); // Convert abcissa to log coordinates if needed picker_min[ch] = _curve_to_mouse(picker_min[ch], g->zoom_factor, g->offset_x); picker_max[ch] = _curve_to_mouse(picker_max[ch], g->zoom_factor, g->offset_x); picker_mean[ch] = _curve_to_mouse(picker_mean[ch], g->zoom_factor, g->offset_x); cairo_set_source_rgba(cr, 0.5, 0.7, 0.5, 0.15); cairo_rectangle(cr, width * picker_min[ch], 0, width * fmax(picker_max[ch] - picker_min[ch], 0.0f), -height); cairo_fill(cr); cairo_set_source_rgba(cr, 0.5, 0.7, 0.5, 0.5); cairo_move_to(cr, width * picker_mean[ch], 0); cairo_line_to(cr, width * picker_mean[ch], -height); cairo_stroke(cr); } } } // ... and the local sample if(self->picked_color_max[ch] >= 0.0f) { PangoLayout *layout; PangoRectangle ink; PangoFontDescription *desc = dt_gui_get_font(); pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD); pango_font_description_set_absolute_size(desc, PANGO_SCALE); layout = pango_cairo_create_layout(cr); pango_layout_set_font_description(layout, desc); picker_scale(self->picked_color, picker_mean, p, work_profile); picker_scale(self->picked_color_min, picker_min, p, work_profile); picker_scale(self->picked_color_max, picker_max, p, work_profile); // scale conservatively to 100% of width: snprintf(text, sizeof(text), "100.00 / 100.00 ( +100.00)"); pango_layout_set_text(layout, text, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); pango_font_description_set_absolute_size(desc, width * 1.0 / ink.width * PANGO_SCALE); pango_layout_set_font_description(layout, desc); picker_min[ch] = _curve_to_mouse(picker_min[ch], g->zoom_factor, g->offset_x); picker_max[ch] = _curve_to_mouse(picker_max[ch], g->zoom_factor, g->offset_x); picker_mean[ch] = _curve_to_mouse(picker_mean[ch], g->zoom_factor, g->offset_x); cairo_set_source_rgba(cr, 0.7, 0.5, 0.5, 0.33); cairo_rectangle(cr, width * picker_min[ch], 0, width * fmax(picker_max[ch] - picker_min[ch], 0.0f), -height); cairo_fill(cr); cairo_set_source_rgba(cr, 0.9, 0.7, 0.7, 0.5); cairo_move_to(cr, width * picker_mean[ch], 0); cairo_line_to(cr, width * picker_mean[ch], -height); cairo_stroke(cr); picker_scale(self->picked_color, picker_mean, p, work_profile); picker_scale(self->picked_output_color, picker_min, p, work_profile); snprintf(text, sizeof(text), "%.1f → %.1f", picker_mean[ch] * 255.f, picker_min[ch] * 255.f); cairo_set_source_rgb(cr, 0.1, 0.1, 0.1); cairo_set_font_size(cr, DT_PIXEL_APPLY_DPI(0.04) * height); pango_layout_set_text(layout, text, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); cairo_move_to(cr, 0.02f * width, -0.94 * height - ink.height - ink.y); pango_cairo_show_layout(cr, layout); cairo_stroke(cr); pango_font_description_free(desc); g_object_unref(layout); } } } // draw zoom info if(darktable.develop->darkroom_skip_mouse_events) { PangoLayout *layout; PangoRectangle ink; PangoFontDescription *desc = dt_gui_get_font(); pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD); pango_font_description_set_absolute_size(desc, PANGO_SCALE); layout = pango_cairo_create_layout(cr); pango_layout_set_font_description(layout, desc); // scale conservatively to 100% of width: snprintf(text, sizeof(text), "zoom: 100 x: 100 y: 100"); pango_layout_set_text(layout, text, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); pango_font_description_set_absolute_size(desc, width * 1.0 / ink.width * PANGO_SCALE); pango_layout_set_font_description(layout, desc); snprintf(text, sizeof(text), "zoom: %i x: %i y: %i", (int)((g->zoom_factor - 1.f) * 100.f), (int)(g->offset_x * 100.f), (int)(g->offset_y * 100.f)); cairo_set_source_rgba(cr, 0.1, 0.1, 0.1, 0.5); pango_layout_set_text(layout, text, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); cairo_move_to(cr, 0.98f * width - ink.width - ink.x, -0.02 * height - ink.height - ink.y); pango_cairo_show_layout(cr, layout); cairo_stroke(cr); pango_font_description_free(desc); g_object_unref(layout); } else if(g->selected >= 0) { // draw information about current selected node PangoLayout *layout; PangoRectangle ink; PangoFontDescription *desc = dt_gui_get_font(); pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD); pango_font_description_set_absolute_size(desc, PANGO_SCALE); layout = pango_cairo_create_layout(cr); pango_layout_set_font_description(layout, desc); // scale conservatively to 100% of width: snprintf(text, sizeof(text), "100.00 / 100.00 ( +100.00)"); pango_layout_set_text(layout, text, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); pango_font_description_set_absolute_size(desc, width * 1.0 / ink.width * PANGO_SCALE); pango_layout_set_font_description(layout, desc); const float min_scale_value = 0.0f; const float max_scale_value = 255.0f; const float x_node_value = curve_nodes[g->selected].x * (max_scale_value - min_scale_value) + min_scale_value; const float y_node_value = curve_nodes[g->selected].y * (max_scale_value - min_scale_value) + min_scale_value; const float d_node_value = y_node_value - x_node_value; snprintf(text, sizeof(text), "%.1f / %.1f ( %+.1f)", x_node_value, y_node_value, d_node_value); cairo_set_source_rgb(cr, 0.1, 0.1, 0.1); pango_layout_set_text(layout, text, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); cairo_move_to(cr, 0.98f * width - ink.width - ink.x, -0.02 * height - ink.height - ink.y); pango_cairo_show_layout(cr, layout); cairo_stroke(cr); pango_font_description_free(desc); g_object_unref(layout); // enlarge selected node cairo_set_source_rgb(cr, .9, .9, .9); const float x = _curve_to_mouse(curve_nodes[g->selected].x, g->zoom_factor, g->offset_x), y = _curve_to_mouse(curve_nodes[g->selected].y, g->zoom_factor, g->offset_y); cairo_arc(cr, x * width, -y * height, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI); cairo_stroke(cr); } // draw curve cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.)); cairo_set_source_rgb(cr, .9, .9, .9); const float y_offset = _curve_to_mouse(g->draw_ys[0], g->zoom_factor, g->offset_y); cairo_move_to(cr, 0, -height * y_offset); for(int k = 1; k < DT_IOP_RGBCURVE_RES; k++) { const float xx = k / (DT_IOP_RGBCURVE_RES - 1.0f); float yy; if(xx > xm) { yy = dt_iop_eval_exp(unbounded_coeffs, xx); } else { yy = g->draw_ys[k]; } const float x = _curve_to_mouse(xx, g->zoom_factor, g->offset_x), y = _curve_to_mouse(yy, g->zoom_factor, g->offset_y); cairo_line_to(cr, x * width, -height * y); } cairo_stroke(cr); finally: cairo_destroy(cr); cairo_set_source_surface(crf, cst, 0, 0); cairo_paint(crf); cairo_surface_destroy(cst); return TRUE; } static void _area_motion_notify_callback(GtkEventControllerMotion *controller, gdouble x, gdouble y, dt_iop_module_t *self) { GtkWidget *widget = dt_gui_get_widget(controller); dt_iop_rgbcurve_gui_data_t *g = self->gui_data; dt_iop_rgbcurve_params_t *p = self->params; const int inset = DT_GUI_CURVE_EDITOR_INSET; // drag the draw area if(darktable.develop->darkroom_skip_mouse_events) { GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); const int height = allocation.height - 2 * inset; const int width = allocation.width - 2 * inset; const float mx = g->mouse_x; const float my = g->mouse_y; g->mouse_x = CLAMP(x - inset, 0, width) / (float)width; g->mouse_y = 1.0 - CLAMP(y - inset, 0, height) / (float)height; if(dt_key_modifier_state() & GDK_BUTTON1_MASK) { g->offset_x += (mx - g->mouse_x) / g->zoom_factor; g->offset_y += (my - g->mouse_y) / g->zoom_factor; g->offset_x = CLAMP(g->offset_x, 0.f, (g->zoom_factor - 1.f) / g->zoom_factor); g->offset_y = CLAMP(g->offset_y, 0.f, (g->zoom_factor - 1.f) / g->zoom_factor); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } } const int ch = g->channel; const int nodes = p->curve_num_nodes[ch]; dt_iop_rgbcurve_node_t *curve_nodes = p->curve_nodes[ch]; // if autoscale is on: do not modify g and b curves if((p->curve_autoscale != DT_S_SCALE_MANUAL_RGB) && g->channel != DT_IOP_RGBCURVE_R) goto finally; GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); const int height = allocation.height - 2 * inset, width = allocation.width - 2 * inset; const double old_m_x = g->mouse_x; const double old_m_y = g->mouse_y; g->mouse_x = CLAMP(x - inset, 0, width) / (float)width; g->mouse_y = 1.0 - CLAMP(y - inset, 0, height) / (float)height; const float mx = g->mouse_x; const float my = g->mouse_y; const float linx = _mouse_to_curve(mx, g->zoom_factor, g->offset_x), liny = _mouse_to_curve(my, g->zoom_factor, g->offset_y); if(dt_key_modifier_state() & GDK_BUTTON1_MASK) { // got a vertex selected: if(g->selected >= 0) { // this is used to translate mause position in loglogscale to // make this behavior unified with linear scale. const float translate_mouse_x = old_m_x - _curve_to_mouse(curve_nodes[g->selected].x, g->zoom_factor, g->offset_x); const float translate_mouse_y = old_m_y - _curve_to_mouse(curve_nodes[g->selected].y, g->zoom_factor, g->offset_y); // dx & dy are in linear coordinates const float dx = _mouse_to_curve(g->mouse_x - translate_mouse_x, g->zoom_factor, g->offset_x) - _mouse_to_curve(old_m_x - translate_mouse_x, g->zoom_factor, g->offset_x); const float dy = _mouse_to_curve(g->mouse_y - translate_mouse_y, g->zoom_factor, g->offset_y) - _mouse_to_curve(old_m_y - translate_mouse_y, g->zoom_factor, g->offset_y); dt_iop_color_picker_reset(self, TRUE); _move_point_internal(self, widget, dx, dy, dt_key_modifier_state()); } else if(nodes < DT_IOP_RGBCURVE_MAXNODES && g->selected >= -1) { dt_iop_color_picker_reset(self, TRUE); // no vertex was close, create a new one! g->selected = _add_node(curve_nodes, &p->curve_num_nodes[ch], linx, liny); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); } } else { // minimum area around the node to select it: float min = .04f * .04f; // comparing against square int nearest = -1; for(int k = 0; k < nodes; k++) { const float dist = (my - _curve_to_mouse(curve_nodes[k].y, g->zoom_factor, g->offset_y)) * (my - _curve_to_mouse(curve_nodes[k].y, g->zoom_factor, g->offset_y)) + (mx - _curve_to_mouse(curve_nodes[k].x, g->zoom_factor, g->offset_x)) * (mx - _curve_to_mouse(curve_nodes[k].x, g->zoom_factor, g->offset_x)); if(dist < min) { min = dist; nearest = k; } } g->selected = nearest; } finally: if(g->selected >= 0) gtk_widget_grab_focus(widget); gtk_widget_queue_draw(widget); } static void _area_button_press_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self) { GtkWidget *widget = dt_gui_get_widget(gesture); dt_iop_rgbcurve_params_t *p = self->params; const dt_iop_rgbcurve_params_t *const d = self->default_params; dt_iop_rgbcurve_gui_data_t *g = self->gui_data; if(darktable.develop->darkroom_skip_mouse_events) return; const int ch = g->channel; const int autoscale = p->curve_autoscale; const int nodes = p->curve_num_nodes[ch]; dt_iop_rgbcurve_node_t *curve_nodes = p->curve_nodes[ch]; if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { if(dt_modifier_is(dt_key_modifier_state(), GDK_CONTROL_MASK) && nodes < DT_IOP_RGBCURVE_MAXNODES && g->selected == -1) { // if we are not on a node -> add a new node at the current x of // the pointer and y of the curve at that x const int inset = DT_GUI_CURVE_EDITOR_INSET; GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); const int width = allocation.width - 2 * inset; const int height = allocation.height - 2 * inset; g->mouse_x = CLAMP(x - inset, 0, width) / (float)width; g->mouse_y = 1.0 - CLAMP(y - inset, 0, height) / (float)height; const float mx = g->mouse_x; const float linx = _mouse_to_curve(mx, g->zoom_factor, g->offset_x); // don't add a node too close to others in x direction, it can // crash dt int selected = -1; if(curve_nodes[0].x > mx) selected = 0; else { for(int k = 1; k < nodes; k++) { if(curve_nodes[k].x > mx) { selected = k; break; } } } if(selected == -1) selected = nodes; // evaluate the curve at the current x position const float curve_y = dt_draw_curve_calc_value(g->minmax_curve[ch], linx); if(curve_y >= 0.0f && curve_y <= 1.0f) // never add something outside the // viewport, you couldn't change it // afterwards { // create a new node selected = _add_node(curve_nodes, &p->curve_num_nodes[ch], linx, curve_y); // maybe set the new one as being selected const float min = .04f * .04f; // comparing against square for(int k = 0; k < nodes; k++) { const float other_y = _curve_to_mouse(curve_nodes[k].y, g->zoom_factor, g->offset_y); const float dist = (curve_y - other_y) * (curve_y - other_y); if(dist < min) g->selected = selected; } dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } } else if(n_press >= 2) { // reset current curve // if autoscale is on: allow only reset of L curve if(!((autoscale != DT_S_SCALE_MANUAL_RGB) && ch != DT_IOP_RGBCURVE_R)) { p->curve_num_nodes[ch] = d->curve_num_nodes[ch]; p->curve_type[ch] = d->curve_type[ch]; for(int k = 0; k < d->curve_num_nodes[ch]; k++) { p->curve_nodes[ch][k].x = d->curve_nodes[ch][k].x; p->curve_nodes[ch][k].y = d->curve_nodes[ch][k].y; } g->selected = -2; // avoid motion notify re-inserting immediately. dt_bauhaus_combobox_set(g->interpolator, p->curve_type[DT_IOP_RGBCURVE_R]); dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } else { if(ch != DT_IOP_RGBCURVE_R) { p->curve_autoscale = DT_S_SCALE_MANUAL_RGB; g->selected = -2; // avoid motion notify re-inserting immediately. dt_bauhaus_combobox_set(g->autoscale, 1); dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } } } } else if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_SECONDARY && g->selected >= 0) { // consume the event so it does not bubble to the module body's // right-click handler (which opens the presets menu) dt_gui_claim(gesture); if(g->selected == 0 || g->selected == nodes - 1) { const float reset_value = g->selected == 0 ? 0.f : 1.f; curve_nodes[g->selected].y = curve_nodes[g->selected].x = reset_value; dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); gtk_widget_queue_draw(GTK_WIDGET(g->area)); return; // an endpoint reset must not fall through into node removal } for(int k = g->selected; k < nodes - 1; k++) { curve_nodes[k].x = curve_nodes[k + 1].x; curve_nodes[k].y = curve_nodes[k + 1].y; } curve_nodes[nodes - 1].x = curve_nodes[nodes - 1].y = 0; g->selected = -2; // avoid re-insertion of that point immediately after this p->curve_num_nodes[ch]--; dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } } void gui_reset(dt_iop_module_t *self) { dt_iop_rgbcurve_gui_data_t *g = self->gui_data; dt_iop_rgbcurve_params_t *p = self->params; g->channel = DT_IOP_RGBCURVE_R; g->selected = -1; g->offset_x = g->offset_y = 0.f; g->zoom_factor = 1.f; dt_bauhaus_combobox_set(g->interpolator, p->curve_type[DT_IOP_RGBCURVE_R]); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } void change_image(dt_iop_module_t *self) { dt_iop_rgbcurve_gui_data_t *g = self->gui_data; if(g) { if(!g->channel) g->channel = DT_IOP_RGBCURVE_R; g->mouse_x = g->mouse_y = -1.0; g->selected = -1; g->offset_x = g->offset_y = 0.f; g->zoom_factor = 1.f; } } void gui_init(dt_iop_module_t *self) { dt_iop_rgbcurve_gui_data_t *g = IOP_GUI_ALLOC(rgbcurve); const dt_iop_rgbcurve_params_t *const p = self->default_params; for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) { g->minmax_curve[ch] = dt_draw_curve_new(0.0, 1.0, p->curve_type[ch]); g->minmax_curve_nodes[ch] = p->curve_num_nodes[ch]; g->minmax_curve_type[ch] = p->curve_type[ch]; for(int k = 0; k < p->curve_num_nodes[ch]; k++) dt_draw_curve_add_point(g->minmax_curve[ch], p->curve_nodes[ch][k].x, p->curve_nodes[ch][k].y); } g->channel = DT_IOP_RGBCURVE_R; change_image(self); g->autoscale = dt_bauhaus_combobox_from_params(self, "curve_autoscale"); gtk_widget_set_tooltip_text(g->autoscale, _("choose between linked and independent channels.")); g->channel_tabs = GTK_NOTEBOOK(gtk_notebook_new()); dt_action_define_iop(self, NULL, N_("channel"), GTK_WIDGET(g->channel_tabs), &dt_action_def_tabs_rgb); dt_ui_notebook_page(g->channel_tabs, N_("R"), _("curve nodes for r channel")); dt_ui_notebook_page(g->channel_tabs, N_("G"), _("curve nodes for g channel")); dt_ui_notebook_page(g->channel_tabs, N_("B"), _("curve nodes for b channel")); g_signal_connect(G_OBJECT(g->channel_tabs), "switch_page", G_CALLBACK(tab_switch_callback), self); // color pickers g->colorpicker = dt_color_picker_new(self, DT_COLOR_PICKER_POINT_AREA | DT_COLOR_PICKER_IO, NULL); gtk_widget_set_tooltip_text(g->colorpicker, _("pick GUI color from image\n" "ctrl+click or right-click to select an area")); gtk_widget_set_name(g->colorpicker, "keep-active"); dt_action_define_iop(self, N_("pickers"), N_("show color"), g->colorpicker, &dt_action_def_color_picker); g->colorpicker_set_values = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_IO, NULL); dtgtk_togglebutton_set_paint(DTGTK_TOGGLEBUTTON(g->colorpicker_set_values), dtgtk_cairo_paint_colorpicker_set_values, 0, NULL); dt_gui_add_class(g->colorpicker_set_values, "dt_transparent_background"); gtk_widget_set_size_request(g->colorpicker_set_values, DT_PIXEL_APPLY_DPI(14), DT_PIXEL_APPLY_DPI(14)); gtk_widget_set_tooltip_text(g->colorpicker_set_values, _("create a curve based on an area from the image\n" "drag to create a flat curve\n" "ctrl+drag to create a positive curve\n" "shift+drag to create a negative curve")); dt_action_define_iop(self, N_("pickers"), N_("create curve"), g->colorpicker_set_values, &dt_action_def_color_picker); g->area = GTK_DRAWING_AREA(dtgtk_drawing_area_new_with_height(0)); g_object_set_data(G_OBJECT(g->area), "iop-instance", self); dt_action_define_iop(self, NULL, N_("curve"), GTK_WIDGET(g->area), NULL); // FIXME: that tooltip goes in the way of the numbers when you hover // a node to get a reading // gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("double click // to reset curve")); gtk_widget_set_can_focus(GTK_WIDGET(g->area), TRUE); g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(_area_draw_callback), self); gtk_widget_add_events(GTK_WIDGET(g->area), GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | darktable.gui->scroll_mask); dt_gui_connect_click(g->area, _area_button_press_callback, NULL, self); dt_gui_connect_motion(g->area, _area_motion_notify_callback, NULL, _area_leave_notify_callback, self); dt_gui_connect_scroll(g->area, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, _area_scrolled_callback, self); dt_gui_connect_key(g->area, _area_key_press_callback, self); g->interpolator = dt_bauhaus_combobox_new_interpolation(self); gtk_widget_set_tooltip_text(g->interpolator, _("change this method if you see oscillations or cusps in the curve\n" "- cubic spline is better to produce smooth curves but oscillates when nodes are too close\n" "- centripetal is better to avoids cusps and oscillations with close nodes but is less smooth\n" "- monotonic is better for accuracy of pure analytical functions (log, gamma, exp)")); g_signal_connect(G_OBJECT(g->interpolator), "value-changed", G_CALLBACK(interpolator_callback), self); dt_gui_box_add(self->widget, dt_gui_hbox(dt_gui_expand(g->channel_tabs), dt_gui_expand(gtk_grid_new()), g->colorpicker, g->colorpicker_set_values), g->area, g->interpolator); g->chk_compensate_middle_grey = dt_bauhaus_toggle_from_params(self, "compensate_middle_grey"); gtk_widget_set_tooltip_text(g->chk_compensate_middle_grey, _("compensate middle gray")); g->cmb_preserve_colors = dt_bauhaus_combobox_from_params(self, "preserve_colors"); gtk_widget_set_tooltip_text(g->cmb_preserve_colors, _("method to preserve colors when applying contrast")); } void gui_update(dt_iop_module_t *self) { dt_iop_rgbcurve_gui_data_t *g = self->gui_data; dt_iop_rgbcurve_params_t *p = self->params; // every GUI handler uses the stored count, and adding a node writes through it for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) p->curve_num_nodes[ch] = _rgbcurve_nodes(p->curve_num_nodes[ch]); dt_bauhaus_combobox_set(g->autoscale, p->curve_autoscale); dt_bauhaus_combobox_set(g->interpolator, p->curve_type[DT_IOP_RGBCURVE_R]); dt_bauhaus_toggle_set(g->chk_compensate_middle_grey, p->compensate_middle_grey); dt_bauhaus_combobox_set(g->cmb_preserve_colors, p->preserve_colors); _rgbcurve_show_hide_controls(p, g); // that's all, gui curve is read directly from params during expose event. gtk_widget_queue_draw(GTK_WIDGET(g->area)); } void gui_cleanup(dt_iop_module_t *self) { dt_iop_rgbcurve_gui_data_t *g = self->gui_data; for(int k = 0; k < DT_IOP_RGBCURVE_MAX_CHANNELS; k++) dt_draw_curve_destroy(g->minmax_curve[k]); } void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { // create part of the pixelpipe dt_iop_rgbcurve_data_t *d = dt_alloc1_align_type(dt_iop_rgbcurve_data_t); const dt_iop_rgbcurve_params_t *const default_params = self->default_params; piece->data = (void *)d; memcpy(&d->params, default_params, sizeof(dt_iop_rgbcurve_params_t)); for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) { d->curve[ch] = dt_draw_curve_new(0.0, 1.0, default_params->curve_type[ch]); d->params.curve_num_nodes[ch] = default_params->curve_num_nodes[ch]; d->params.curve_type[ch] = default_params->curve_type[ch]; for(int k = 0; k < default_params->curve_num_nodes[ch]; k++) dt_draw_curve_add_point(d->curve[ch], default_params->curve_nodes[ch][k].x, default_params->curve_nodes[ch][k].y); } for(int k = 0; k < 0x10000; k++) d->table[DT_IOP_RGBCURVE_R][k] = k / 0x10000; // identity for r for(int k = 0; k < 0x10000; k++) d->table[DT_IOP_RGBCURVE_G][k] = k / 0x10000; // identity for g for(int k = 0; k < 0x10000; k++) d->table[DT_IOP_RGBCURVE_B][k] = k / 0x10000; // identity for b } void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { // clean up everything again. dt_iop_rgbcurve_data_t *d = piece->data; for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) dt_draw_curve_destroy(d->curve[ch]); dt_free_align(piece->data); piece->data = NULL; } void init(dt_iop_module_t *self) { dt_iop_default_init(self); self->request_histogram |= (DT_REQUEST_ON | DT_REQUEST_EXPANDED); dt_iop_rgbcurve_params_t *d = self->default_params; d->curve_nodes[0][1].x = d->curve_nodes[0][1].y = d->curve_nodes[1][1].x = d->curve_nodes[1][1].y = d->curve_nodes[2][1].x = d->curve_nodes[2][1].y = 1.0; self->histogram_middle_grey = d->compensate_middle_grey; } void init_global(dt_iop_module_so_t *self) { const int program = 25; // rgbcurve.cl, from programs.conf dt_iop_rgbcurve_global_data_t *gd =dt_alloc1_align_type(dt_iop_rgbcurve_global_data_t); self->data = gd; gd->kernel_rgbcurve = dt_opencl_create_kernel(program, "rgbcurve"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_rgbcurve_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_rgbcurve); dt_free_align(self->data); self->data = NULL; } // called from process*(), takes care of changed curve type static void _generate_curve_lut(dt_dev_pixelpipe_t *pipe, dt_iop_rgbcurve_data_t *d) { const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(pipe); DT_ALIGNED_ARRAY dt_iop_rgbcurve_node_t curve_nodes[3][DT_IOP_RGBCURVE_MAXNODES]; if(work_profile) { if(d->type_work == work_profile->type && strcmp(d->filename_work, work_profile->filename) == 0) return; } if(work_profile && d->params.compensate_middle_grey) { d->type_work = work_profile->type; g_strlcpy(d->filename_work, work_profile->filename, sizeof(d->filename_work)); for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) { for(int k = 0; k < d->params.curve_num_nodes[ch]; k++) { curve_nodes[ch][k].x = dt_ioppr_uncompensate_middle_grey(d->params.curve_nodes[ch][k].x, work_profile); curve_nodes[ch][k].y = dt_ioppr_uncompensate_middle_grey(d->params.curve_nodes[ch][k].y, work_profile); } } } else { for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) { memcpy(curve_nodes[ch], d->params.curve_nodes[ch], sizeof(dt_iop_rgbcurve_node_t) * DT_IOP_RGBCURVE_MAXNODES); } } for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) { /* take care of possible change of curve type, number of nodes is explicitly set. We only need a new curve if it's type has changed for a different interpolation. If we change the curve we avoid a possible race condition between pixelpipes. */ if(d->curve_changed[ch]) { dt_draw_curve_t *oldcurve = d->curve[ch]; d->curve[ch] = dt_draw_curve_new(0.0, 1.0, d->params.curve_type[ch]); d->curve_changed[ch] = FALSE; dt_draw_curve_destroy(oldcurve); } for(int k = 0; k < d->params.curve_num_nodes[ch]; k++) dt_draw_curve_set_point(d->curve[ch], k, curve_nodes[ch][k].x, curve_nodes[ch][k].y); d->curve[ch]->c.m_numAnchors = d->params.curve_num_nodes[ch]; dt_draw_curve_calc_values(d->curve[ch], 0.0f, 1.0f, 0x10000, NULL, d->table[ch]); } // extrapolation for each curve (right hand side only): for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) { const float xm_L = curve_nodes[ch][d->params.curve_num_nodes[ch] - 1].x; const float x_L[4] = { 0.7f * xm_L, 0.8f * xm_L, 0.9f * xm_L, 1.0f * xm_L }; const float y_L[4] = { d->table[ch][CLAMP((int)(x_L[0] * 0x10000ul), 0, 0xffff)], d->table[ch][CLAMP((int)(x_L[1] * 0x10000ul), 0, 0xffff)], d->table[ch][CLAMP((int)(x_L[2] * 0x10000ul), 0, 0xffff)], d->table[ch][CLAMP((int)(x_L[3] * 0x10000ul), 0, 0xffff)] }; dt_iop_estimate_exp(x_L, y_L, 4, d->unbounded_coeffs[ch]); } } 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_rgbcurve_data_t *d = piece->data; dt_iop_rgbcurve_params_t *p = (dt_iop_rgbcurve_params_t *)p1; if(dt_pipe_is_preview(pipe)) { piece->request_histogram |= DT_REQUEST_ON; self->histogram_middle_grey = p->compensate_middle_grey; } else { piece->request_histogram &= ~DT_REQUEST_ON; } for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) d->curve_changed[ch] = d->params.curve_type[ch] != p->curve_type[ch]; memcpy(&d->params, p, sizeof(dt_iop_rgbcurve_params_t)); for(int ch = 0; ch < DT_IOP_RGBCURVE_MAX_CHANNELS; ch++) d->params.curve_num_nodes[ch] = _rgbcurve_nodes(d->params.curve_num_nodes[ch]); // working color profile d->type_work = DT_COLORSPACE_NONE; d->filename_work[0] = '\0'; } #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) { const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(piece->pipe); dt_iop_rgbcurve_data_t *d = piece->data; dt_iop_rgbcurve_global_data_t *gd = self->global_data; _generate_curve_lut(piece->pipe, d); cl_int err = CL_SUCCESS; cl_mem dev_r = NULL; cl_mem dev_g = NULL; cl_mem dev_b = NULL; cl_mem dev_coeffs_r = NULL; cl_mem dev_coeffs_g = NULL; cl_mem dev_coeffs_b = NULL; cl_mem dev_profile_info = NULL; cl_mem dev_profile_lut = NULL; dt_colorspaces_iccprofile_info_cl_t *profile_info_cl; cl_float *profile_lut_cl = NULL; const int use_work_profile = (work_profile == NULL) ? 0 : 1; const int devid = piece->pipe->devid; const int width = roi_in->width; const int height = roi_in->height; const int autoscale = d->params.curve_autoscale; const int preserve_colors = d->params.preserve_colors; err = dt_ioppr_build_iccprofile_params_cl(work_profile, devid, &profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut); if(err != CL_SUCCESS) goto cleanup; err = CL_MEM_OBJECT_ALLOCATION_FAILURE; dev_r = dt_opencl_copy_host_to_image(devid, d->table[DT_IOP_RGBCURVE_R], 256, 256, sizeof(float)); if(dev_r == NULL) goto cleanup; dev_g = dt_opencl_copy_host_to_image(devid, d->table[DT_IOP_RGBCURVE_G], 256, 256, sizeof(float)); if(dev_g == NULL) goto cleanup; dev_b = dt_opencl_copy_host_to_image(devid, d->table[DT_IOP_RGBCURVE_B], 256, 256, sizeof(float)); if(dev_b == NULL) goto cleanup; dev_coeffs_r = dt_opencl_copy_host_to_device_constant (devid, sizeof(float) * DT_IOP_RGBCURVE_MAX_CHANNELS, d->unbounded_coeffs[0]); if(dev_coeffs_r == NULL) goto cleanup; dev_coeffs_g = dt_opencl_copy_host_to_device_constant (devid, sizeof(float) * DT_IOP_RGBCURVE_MAX_CHANNELS, d->unbounded_coeffs[1]); if(dev_coeffs_g == NULL) goto cleanup; dev_coeffs_b = dt_opencl_copy_host_to_device_constant (devid, sizeof(float) * DT_IOP_RGBCURVE_MAX_CHANNELS, d->unbounded_coeffs[2]); if(dev_coeffs_b == NULL) goto cleanup; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_rgbcurve, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(dev_r), CLARG(dev_g), CLARG(dev_b), CLARG(dev_coeffs_r), CLARG(dev_coeffs_g), CLARG(dev_coeffs_b), CLARG(autoscale), CLARG(preserve_colors), CLARG(dev_profile_info), CLARG(dev_profile_lut), CLARG(use_work_profile)); cleanup: dt_opencl_release_mem_object(dev_r); dt_opencl_release_mem_object(dev_g); dt_opencl_release_mem_object(dev_b); dt_opencl_release_mem_object(dev_coeffs_r); dt_opencl_release_mem_object(dev_coeffs_g); dt_opencl_release_mem_object(dev_coeffs_b); dt_ioppr_free_iccprofile_params_cl(&profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut); return err; } #endif void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(piece->pipe); const float *const restrict in = (float*)ivoid; float *const restrict out = (float*)ovoid; if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, in, out, roi_in, roi_out)) return; // image has been copied through to output and module's // trouble flag has been updated dt_iop_rgbcurve_data_t *const restrict d = piece->data; _generate_curve_lut(piece->pipe, d); const float xm_L = 1.0f / d->unbounded_coeffs[DT_IOP_RGBCURVE_R][0]; const float xm_g = 1.0f / d->unbounded_coeffs[DT_IOP_RGBCURVE_G][0]; const float xm_b = 1.0f / d->unbounded_coeffs[DT_IOP_RGBCURVE_B][0]; const int width = roi_out->width; const int height = roi_out->height; const size_t npixels = (size_t)width * height; const int autoscale = d->params.curve_autoscale; const _curve_table_ptr restrict table = d->table; const _coeffs_table_ptr restrict unbounded_coeffs = d->unbounded_coeffs; DT_OMP_FOR() for(size_t y = 0; y < 4*npixels; y += 4) { if(autoscale == DT_S_SCALE_MANUAL_RGB) { out[y+0] = (in[y+0] < xm_L) ? table[DT_IOP_RGBCURVE_R][CLAMP((int)(in[y+0] * 0x10000ul), 0, 0xffff)] : dt_iop_eval_exp(unbounded_coeffs[DT_IOP_RGBCURVE_R], in[y+0]); out[y+1] = (in[y+1] < xm_g) ? table[DT_IOP_RGBCURVE_G][CLAMP((int)(in[y+1] * 0x10000ul), 0, 0xffff)] : dt_iop_eval_exp(unbounded_coeffs[DT_IOP_RGBCURVE_G], in[y+1]); out[y+2] = (in[y+2] < xm_b) ? table[DT_IOP_RGBCURVE_B][CLAMP((int)(in[y+2] * 0x10000ul), 0, 0xffff)] : dt_iop_eval_exp(unbounded_coeffs[DT_IOP_RGBCURVE_B], in[y+2]); } else if(autoscale == DT_S_SCALE_AUTOMATIC_RGB) { if(d->params.preserve_colors == DT_RGB_NORM_NONE) { for(int c = 0; c < 3; c++) { out[y+c] = (in[y+c] < xm_L) ? table[DT_IOP_RGBCURVE_R][CLAMP((int)(in[y+c] * 0x10000ul), 0, 0xffff)] : dt_iop_eval_exp(unbounded_coeffs[DT_IOP_RGBCURVE_R], in[y+c]); } } else { float ratio = 1.f; const float lum = dt_rgb_norm(in + y, d->params.preserve_colors, work_profile); if(lum > 0.f) { const float curve_lum = (lum < xm_L) ? table[DT_IOP_RGBCURVE_R][CLAMP((int)(lum * 0x10000ul), 0, 0xffff)] : dt_iop_eval_exp(unbounded_coeffs[DT_IOP_RGBCURVE_R], lum); ratio = curve_lum / lum; } for(size_t c = 0; c < 3; c++) { out[y+c] = (ratio * in[y+c]); } } } out[y+3] = in[y+3]; } } #undef DT_GUI_CURVE_EDITOR_INSET #undef DT_IOP_RGBCURVE_RES #undef DT_IOP_RGBCURVE_MAXNODES #undef DT_IOP_RGBCURVE_MIN_X_DISTANCE #undef DT_IOP_COLOR_ICC_LEN // 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