/* 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 "common/iop_profile.h" #include "bauhaus/bauhaus.h" #include "common/colorspaces_inline_conversions.h" #include "common/rgb_norms.h" #include "develop/imageop.h" #include "develop/imageop_gui.h" #include "dtgtk/drawingarea.h" #include "gui/accelerators.h" #include "gui/color_picker_proxy.h" #include "libs/colorpicker.h" #define DT_GUI_CURVE_EDITOR_INSET DT_PIXEL_APPLY_DPI(5) DT_MODULE_INTROSPECTION(1, dt_iop_rgblevels_params_t) #define RGBLEVELS_MIN 0.f #define RGBLEVELS_MID 0.5f #define RGBLEVELS_MAX 1.f typedef enum dt_iop_rgblevels_channel_t { DT_IOP_RGBLEVELS_R = 0, DT_IOP_RGBLEVELS_G = 1, DT_IOP_RGBLEVELS_B = 2, DT_IOP_RGBLEVELS_MAX_CHANNELS = 3 } dt_iop_rgblevels_channel_t; typedef enum dt_iop_rgblevels_autoscale_t { DT_IOP_RGBLEVELS_LINKED_CHANNELS = 0, // $DESCRIPTION: "RGB, linked channels" DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS = 1 // $DESCRIPTION: "RGB, independent channels" } dt_iop_rgblevels_autoscale_t; typedef struct dt_iop_rgblevels_params_t { dt_iop_rgblevels_autoscale_t autoscale; // $DEFAULT: DT_IOP_RGBLEVELS_LINKED_CHANNELS $DESCRIPTION: "mode" (DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS, DT_IOP_RGBLEVELS_LINKED_CHANNELS) dt_iop_rgb_norms_t preserve_colors; // $DEFAULT: DT_RGB_NORM_LUMINANCE $DESCRIPTION: "preserve colors" float levels[DT_IOP_RGBLEVELS_MAX_CHANNELS][3]; } dt_iop_rgblevels_params_t; typedef struct dt_iop_rgblevels_gui_data_t { dt_iop_rgblevels_params_t params; GtkWidget *cmb_autoscale; // (DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS, DT_IOP_RGBLEVELS_LINKED_CHANNELS) GtkDrawingArea *area; GtkWidget *cmb_preserve_colors; GtkNotebook *channel_tabs; GtkWidget *bt_auto_levels; GtkWidget *bt_select_region; int call_auto_levels; // should we calculate levels automatically? int draw_selected_region; // are we drawing the selected region? float posx_from, posx_to, posy_from, posy_to; // coordinates of the area dt_boundingbox_t box_cood; // normalized coordinates int button_down; // user pressed the mouse button? double mouse_x, mouse_y; int dragging, handle_move; float drag_start_percentage; dt_iop_rgblevels_channel_t channel; float last_picked_color; GtkWidget *blackpick, *greypick, *whitepick; } dt_iop_rgblevels_gui_data_t; typedef struct dt_iop_rgblevels_data_t { dt_iop_rgblevels_params_t params; float inv_gamma[DT_IOP_RGBLEVELS_MAX_CHANNELS]; float lut[DT_IOP_RGBLEVELS_MAX_CHANNELS][0x10000]; } dt_iop_rgblevels_data_t; typedef struct dt_iop_rgblevels_global_data_t { int kernel_levels; } dt_iop_rgblevels_global_data_t; const char *name() { return _("rgb levels"); } int default_group() { return IOP_GROUP_TONE | IOP_GROUP_GRADING; } int flags() { return IOP_FLAGS_SUPPORTS_BLENDING; } dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return IOP_CS_RGB; } const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("adjust black, white and mid-gray points in RGB color space"), _("corrective and creative"), _("linear, RGB, display-referred"), _("non-linear, RGB"), _("non-linear, RGB, display-referred")); } static void _turn_select_region_off(dt_iop_module_t *self) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; if(g) { g->button_down = g->draw_selected_region = 0; gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_select_region), g->draw_selected_region); } } static void _turn_selregion_picker_off(dt_iop_module_t *self) { _turn_select_region_off(self); dt_iop_color_picker_reset(self, TRUE); } static void _develop_ui_pipe_finished_callback(gpointer instance, dt_iop_module_t *self) { dt_iop_rgblevels_params_t *p = self->params; dt_iop_rgblevels_gui_data_t *g = self->gui_data; if(g == NULL) return; // FIXME: this doesn't seems the right place to update params and GUI ... // update auto levels dt_iop_gui_enter_critical_section(self); if(g->call_auto_levels == 2) { g->call_auto_levels = -1; dt_iop_gui_leave_critical_section(self); memcpy(p, &g->params, sizeof(dt_iop_rgblevels_params_t)); dt_dev_add_history_item(darktable.develop, self, TRUE); dt_iop_gui_enter_critical_section(self); g->call_auto_levels = 0; dt_iop_gui_leave_critical_section(self); DT_ENTER_GUI_UPDATE(); gui_update(self); DT_LEAVE_GUI_UPDATE(); } else { dt_iop_gui_leave_critical_section(self); } } static void _compute_lut(dt_dev_pixelpipe_iop_t *piece) { dt_iop_rgblevels_data_t *d = piece->data; // Building the lut for values in the [0,1] range if(d->params.autoscale == DT_IOP_RGBLEVELS_LINKED_CHANNELS) { const int c = 0; const float delta = (d->params.levels[c][2] - d->params.levels[c][0]) / 2.0f; const float mid = d->params.levels[c][0] + delta; const float tmp = (d->params.levels[c][1] - mid) / delta; d->inv_gamma[0] = d->inv_gamma[1] = d->inv_gamma[2] = pow(10, tmp); for(unsigned int i = 0; i < 0x10000; i++) { float percentage = (float)i / (float)0x10000ul; d->lut[0][i] = d->lut[1][i] = d->lut[2][i] = pow(percentage, d->inv_gamma[c]); } } else { for(int c = 0; c < 3; c++) { const float delta = (d->params.levels[c][2] - d->params.levels[c][0]) / 2.0f; const float mid = d->params.levels[c][0] + delta; const float tmp = (d->params.levels[c][1] - mid) / delta; d->inv_gamma[c] = pow(10, tmp); for(unsigned int i = 0; i < 0x10000; i++) { float percentage = (float)i / (float)0x10000ul; d->lut[c][i] = pow(percentage, d->inv_gamma[c]); } } } } static void _rgblevels_show_hide_controls(dt_iop_rgblevels_params_t *p, dt_iop_rgblevels_gui_data_t *g) { switch(p->autoscale) { case DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS: gtk_notebook_set_show_tabs(g->channel_tabs, TRUE); break; case DT_IOP_RGBLEVELS_LINKED_CHANNELS: gtk_notebook_set_show_tabs(g->channel_tabs, FALSE); break; } if(p->autoscale == DT_IOP_RGBLEVELS_LINKED_CHANNELS) gtk_widget_set_visible(g->cmb_preserve_colors, TRUE); else gtk_widget_set_visible(g->cmb_preserve_colors, FALSE); } int mouse_moved(dt_iop_module_t *self, const float pzx, const float pzy, const double pressure, const int which, const float zoom_scale) { int handled = 0; dt_iop_rgblevels_gui_data_t *g = self->gui_data; if(g && g->draw_selected_region && g->button_down && self->enabled) { float wd, ht; dt_dev_get_preview_size(self->dev, &wd, &ht); g->posx_to = pzx * wd; g->posy_to = pzy * ht; dt_control_queue_redraw_center(); handled = 1; } return handled; } int button_released(dt_iop_module_t *self, const float x, const float y, const int which, const uint32_t state, const float zoom_scale) { int handled = 0; dt_iop_rgblevels_gui_data_t *g = self->gui_data; if(g && g->draw_selected_region && self->enabled) { if(fabsf(g->posx_from - g->posx_to) > 1 && fabsf(g->posy_from - g->posy_to) > 1) { g->box_cood[0] = g->posx_from; g->box_cood[1] = g->posy_from; g->box_cood[2] = g->posx_to; g->box_cood[3] = g->posy_to; dt_dev_distort_backtransform(darktable.develop, g->box_cood, 2); g->box_cood[0] /= darktable.develop->preview_pipe->iwidth; g->box_cood[1] /= darktable.develop->preview_pipe->iheight; g->box_cood[2] /= darktable.develop->preview_pipe->iwidth; g->box_cood[3] /= darktable.develop->preview_pipe->iheight; g->button_down = 0; g->call_auto_levels = 1; dt_dev_reprocess_all(self->dev); } else g->button_down = 0; handled = 1; } return handled; } int button_pressed(dt_iop_module_t *self, const float pzx, const float pzy, const double pressure, const int which, const int type, const uint32_t state, const float zoom_scale) { int handled = 0; dt_iop_rgblevels_gui_data_t *g = self->gui_data; if(g && g->draw_selected_region && self->enabled) { if((which == GDK_BUTTON_SECONDARY) || (which == GDK_BUTTON_PRIMARY && type == GDK_2BUTTON_PRESS)) { _turn_selregion_picker_off(self); handled = 1; } else if(which == GDK_BUTTON_PRIMARY) { float wd, ht; dt_dev_get_preview_size(self->dev, &wd, &ht); g->posx_from = g->posx_to = pzx * wd; g->posy_from = g->posy_to = pzy * ht; g->button_down = 1; handled = 1; } } return handled; } void gui_post_expose(dt_iop_module_t *self, cairo_t *cr, const float width, const float height, const float pointerx, const float pointery, const float zoom_scale) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; if(g == NULL || !self->enabled) return; if(!g->draw_selected_region || !g->button_down) return; if(g->posx_from == g->posx_to && g->posy_from == g->posy_to) return; const float posx_from = fmin(g->posx_from, g->posx_to); const float posx_to = fmax(g->posx_from, g->posx_to); const float posy_from = fmin(g->posy_from, g->posy_to); const float posy_to = fmax(g->posy_from, g->posy_to); cairo_set_line_width(cr, 1.0 / zoom_scale); cairo_set_source_rgb(cr, .2, .2, .2); cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); cairo_rectangle(cr, posx_from, posy_from, (posx_to - posx_from), (posy_to - posy_from)); cairo_stroke(cr); cairo_translate(cr, 1.0 / zoom_scale, 1.0 / zoom_scale); cairo_set_source_rgb(cr, .8, .8, .8); cairo_rectangle(cr, posx_from + 1.0 / zoom_scale, posy_from, (posx_to - posx_from) - 3. / zoom_scale, (posy_to - posy_from) - 2. / zoom_scale); cairo_stroke(cr); } static void _area_leave_notify_callback(GtkEventControllerMotion *controller, dt_iop_module_t *self) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; g->mouse_x = g->mouse_y = -1.0; 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_rgblevels_gui_data_t *g = self->gui_data; dt_iop_rgblevels_params_t *p = self->params; const int inset = DT_GUI_CURVE_EDITOR_INSET; GtkAllocation allocation; gtk_widget_get_allocation(GTK_WIDGET(g->area), &allocation); int width = allocation.width; int height = allocation.height - DT_RESIZE_HANDLE_SIZE; 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; 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); dt_draw_vertical_lines(cr, 4, 0, 0, width, height); // Drawing the vertical line indicators cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.)); for(int k = 0; k < 3; k++) { if(k == g->handle_move && g->mouse_x > 0) cairo_set_source_rgb(cr, 1, 1, 1); else cairo_set_source_rgb(cr, .7, .7, .7); cairo_move_to(cr, width * p->levels[g->channel][k], height); cairo_rel_line_to(cr, 0, -height); cairo_stroke(cr); } // draw x positions cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.)); const float arrw = DT_PIXEL_APPLY_DPI(7.0f); for(int k = 0; k < 3; k++) { switch(k) { case 0: cairo_set_source_rgb(cr, 0, 0, 0); break; case 1: cairo_set_source_rgb(cr, 0.5, 0.5, 0.5); break; default: cairo_set_source_rgb(cr, 1, 1, 1); break; } cairo_move_to(cr, width * p->levels[g->channel][k], height + inset - 1); cairo_rel_line_to(cr, -arrw * .5f, 0); cairo_rel_line_to(cr, arrw * .5f, -arrw); cairo_rel_line_to(cr, arrw * .5f, arrw); cairo_close_path(cr); if(g->handle_move == k && g->mouse_x > 0) cairo_fill(cr); else cairo_stroke(cr); } cairo_translate(cr, 0, height); // draw histogram in background // only if the module is enabled if(self->enabled) { const int ch = g->channel; const uint32_t *hist = self->histogram; const gboolean is_linear = darktable.lib->proxy.histogram.is_linear; float hist_max; if(p->autoscale == DT_IOP_RGBLEVELS_LINKED_CHANNELS) 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(p->autoscale == DT_IOP_RGBLEVELS_LINKED_CHANNELS) { cairo_set_operator(cr, CAIRO_OPERATOR_ADD); for(int k=DT_IOP_RGBLEVELS_R; kgraph_colors[k]); dt_draw_histogram_8(cr, hist, 4, k, is_linear); } } else if(p->autoscale == DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS) { set_color(cr, darktable.bauhaus->graph_colors[ch]); dt_draw_histogram_8(cr, hist, 4, ch, is_linear); } cairo_pop_group_to_source(cr); cairo_paint_with_alpha(cr, 0.2); } } // Cleaning up cairo_destroy(cr); cairo_set_source_surface(crf, cst, 0, 0); cairo_paint(crf); cairo_surface_destroy(cst); return FALSE; } static void _rgblevels_move_handle(dt_iop_module_t *self, const int handle_move, const float new_pos, float *levels, const float drag_start_percentage) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; float min_x = 0.f; float max_x = 1.f; if((handle_move < 0) || handle_move > 2) return; if(levels == NULL) return; // Determining the minimum and maximum bounds for the drag handles switch(handle_move) { case 0: max_x = fminf(levels[2] - (0.05 / drag_start_percentage), 1); max_x = fminf((levels[2] * (1 - drag_start_percentage) - 0.05) / (1 - drag_start_percentage), max_x); break; case 1: min_x = levels[0] + 0.05; max_x = levels[2] - 0.05; break; case 2: min_x = fmaxf((0.05 / drag_start_percentage) + levels[0], 0); min_x = fmaxf((levels[0] * (1 - drag_start_percentage) + 0.05) / (1 - drag_start_percentage), min_x); break; } levels[handle_move] = fminf(max_x, fmaxf(min_x, new_pos)); if(handle_move != 1) levels[1] = levels[0] + (drag_start_percentage * (levels[2] - levels[0])); g->last_picked_color = -1; dt_dev_add_history_item(darktable.develop, self, TRUE); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } static void _area_motion_notify_callback(GtkEventControllerMotion *controller, gdouble x, gdouble y, dt_iop_module_t *self) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_rgblevels_params_t *p = self->params; GtkWidget *widget = dt_gui_get_widget(controller); const int inset = DT_GUI_CURVE_EDITOR_INSET; GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); int height = allocation.height - 2 * inset - DT_RESIZE_HANDLE_SIZE, width = allocation.width - 2 * inset; if(!g->dragging) { g->mouse_x = CLAMP(x - inset, 0, width); g->drag_start_percentage = (p->levels[g->channel][1] - p->levels[g->channel][0]) / (p->levels[g->channel][2] - p->levels[g->channel][0]); } g->mouse_y = CLAMP(y - inset, 0, height); if(g->dragging) { if(g->handle_move >= 0 && g->handle_move < 3) { const float mx = (CLAMP(x - inset, 0, width)) / (float)width; _rgblevels_move_handle(self, g->handle_move, mx, p->levels[g->channel], g->drag_start_percentage); } } else { g->handle_move = 0; const float mx = CLAMP(x - inset, 0, width) / (float)width; float dist = fabsf(p->levels[g->channel][0] - mx); for(int k = 1; k < 3; k++) { float d2 = fabsf(p->levels[g->channel][k] - mx); if(d2 < dist) { g->handle_move = k; dist = d2; } } darktable.control->element = g->handle_move; 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) { if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_PRIMARY) return; if(darktable.develop->gui_module != self) dt_iop_request_focus(self); if(n_press >= 2) { _turn_selregion_picker_off(self); // Reset dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_rgblevels_params_t *p = self->params; const dt_iop_rgblevels_params_t *const default_params = self->default_params; for(int i = 0; i < 3; i++) p->levels[g->channel][i] = default_params->levels[g->channel][i]; // Needed in case the user scrolls or drags immediately after a reset, // as drag_start_percentage is only updated when the mouse is moved. g->drag_start_percentage = 0.5; dt_dev_add_history_item(darktable.develop, self, TRUE); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } else { _turn_selregion_picker_off(self); dt_iop_rgblevels_gui_data_t *g = self->gui_data; g->dragging = 1; } } static void _area_button_release_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self) { if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_PRIMARY) return; dt_iop_rgblevels_gui_data_t *g = self->gui_data; g->dragging = 0; } static void _area_scroll_callback(GtkEventControllerScroll *controller, gdouble dx, gdouble dy, dt_iop_module_t *self) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_rgblevels_params_t *p = self->params; GtkWidget *widget = dt_gui_get_widget(controller); _turn_selregion_picker_off(self); if(g->dragging) return; if(darktable.develop->gui_module != self) dt_iop_request_focus(self); if(dy != 0.0) { const float interval = 0.002 * dt_accel_get_speed_multiplier(widget, dt_key_modifier_state()); const float new_position = p->levels[g->channel][g->handle_move] - interval * dy; _rgblevels_move_handle(self, g->handle_move, new_position, p->levels[g->channel], g->drag_start_percentage); } } static void _auto_levels_callback(GtkButton *button, dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_request_focus(self); if(self->off) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), 1); dt_dev_add_history_item(darktable.develop, self, TRUE); } _turn_selregion_picker_off(self); dt_iop_gui_enter_critical_section(self); if(g->call_auto_levels == 0) { g->box_cood[0] = g->box_cood[1] = g->box_cood[2] = g->box_cood[3] = 0.f; g->call_auto_levels = 1; } dt_iop_gui_leave_critical_section(self); dt_dev_reprocess_all(self->dev); } static void _select_region_toggled_callback(GtkToggleButton *togglebutton, dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_request_focus(self); if(self->off) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), 1); dt_dev_add_history_item(darktable.develop, self, TRUE); } dt_iop_color_picker_reset(self, TRUE); dt_iop_gui_enter_critical_section(self); if(gtk_toggle_button_get_active(togglebutton)) { g->draw_selected_region = 1; } else g->draw_selected_region = 0; g->posx_from = g->posx_to = g->posy_from = g->posy_to = 0; dt_iop_gui_leave_critical_section(self); } void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_rgblevels_params_t *p = self->params; _turn_selregion_picker_off(self); if(w == g->cmb_autoscale) { g->channel = DT_IOP_RGBLEVELS_R; gtk_notebook_set_current_page(GTK_NOTEBOOK(g->channel_tabs), g->channel); _rgblevels_show_hide_controls(p, g); } } static void _tab_switch_callback(GtkNotebook *notebook, GtkWidget *page, const guint page_num, dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_rgblevels_gui_data_t *g = self->gui_data; g->channel = (dt_iop_rgblevels_channel_t)page_num; gtk_widget_queue_draw(GTK_WIDGET(g->area)); } static void _color_picker_callback(GtkWidget *button, dt_iop_module_t *self) { _turn_select_region_off(self); } void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_t *pipe) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_rgblevels_params_t *p = self->params; const dt_iop_rgblevels_channel_t channel = g->channel; /* we need to save the last picked color to prevent flickering when * changing from one picker to another, as the picked_color value does not * update as rapidly */ const float mean_picked_color = *self->picked_color; if(mean_picked_color != g->last_picked_color) { dt_aligned_pixel_t previous_color; previous_color[0] = p->levels[channel][0]; previous_color[1] = p->levels[channel][1]; previous_color[2] = p->levels[channel][2]; g->last_picked_color = mean_picked_color; if(picker == g->blackpick) { if(mean_picked_color > p->levels[channel][1]) { p->levels[channel][0] = p->levels[channel][1] - FLT_EPSILON; } else { p->levels[channel][0] = mean_picked_color; } } else if(picker == g->greypick) { if(mean_picked_color < p->levels[channel][0] || mean_picked_color > p->levels[channel][2]) { p->levels[channel][1] = p->levels[channel][1]; } else { p->levels[channel][1] = mean_picked_color; } } else if(picker == g->whitepick) { if(mean_picked_color < p->levels[channel][1]) { p->levels[channel][2] = p->levels[channel][1] + FLT_EPSILON; } else { p->levels[channel][2] = mean_picked_color; } } if(previous_color[0] != p->levels[channel][0] || previous_color[1] != p->levels[channel][1] || previous_color[2] != p->levels[channel][2]) { dt_dev_add_history_item(darktable.develop, self, TRUE); } } } 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_rgblevels_data_t *d = piece->data; dt_iop_rgblevels_params_t *p = (dt_iop_rgblevels_params_t *)p1; if(dt_pipe_is_preview(pipe)) piece->request_histogram |= DT_REQUEST_ON; else piece->request_histogram &= ~DT_REQUEST_ON; piece->request_histogram |= DT_REQUEST_EXPANDED; memcpy(&(d->params), p, sizeof(dt_iop_rgblevels_params_t)); for(int i = 0; i < DT_IOP_RGBLEVELS_MAX_CHANNELS; i++) { for(int c = 0; c < 3; c++) { if(d->params.autoscale == DT_IOP_RGBLEVELS_LINKED_CHANNELS) d->params.levels[i][c] = p->levels[0][c]; else d->params.levels[i][c] = p->levels[i][c]; } } _compute_lut(piece); } void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = malloc(sizeof(dt_iop_rgblevels_data_t)); } void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { // clean up everything again. free(piece->data); piece->data = NULL; } void gui_update(dt_iop_module_t *self) { dt_iop_rgblevels_params_t *p = self->params; dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_bauhaus_combobox_set(g->cmb_autoscale, p->autoscale); dt_bauhaus_combobox_set(g->cmb_preserve_colors, p->preserve_colors); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_select_region), g->draw_selected_region); _rgblevels_show_hide_controls(p, g); gtk_widget_queue_draw(GTK_WIDGET(g->area)); } void gui_focus(dt_iop_module_t *self, gboolean in) { if(!in) _turn_select_region_off(self); } void gui_reset(dt_iop_module_t *self) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; _turn_selregion_picker_off(self); g->channel = DT_IOP_RGBLEVELS_R; gtk_widget_queue_draw(GTK_WIDGET(g->area)); } void init(dt_iop_module_t *self) { dt_iop_default_init(self); self->request_histogram |= (DT_REQUEST_ON | DT_REQUEST_EXPANDED); dt_iop_rgblevels_params_t *d = self->default_params; for(int c = 0; c < DT_IOP_RGBLEVELS_MAX_CHANNELS; c++) { d->levels[c][0] = RGBLEVELS_MIN; d->levels[c][1] = RGBLEVELS_MID; d->levels[c][2] = RGBLEVELS_MAX; } } void init_global(dt_iop_module_so_t *self) { const int program = 29; // rgblevels.cl, from programs.conf dt_iop_rgblevels_global_data_t *gd = malloc(sizeof(dt_iop_rgblevels_global_data_t)); self->data = gd; gd->kernel_levels = dt_opencl_create_kernel(program, "rgblevels"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_rgblevels_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_levels); free(self->data); self->data = NULL; } void change_image(dt_iop_module_t *self) { dt_iop_rgblevels_gui_data_t *g = self->gui_data; g->channel = DT_IOP_RGBLEVELS_R; g->call_auto_levels = 0; g->draw_selected_region = 0; g->posx_from = g->posx_to = g->posy_from = g->posy_to = 0.f; g->box_cood[0] = g->box_cood[1] = g->box_cood[2] = g->box_cood[3] = 0.f; g->button_down = 0; } const dt_action_element_def_t _action_elements_levels[] = { { N_("black"), dt_action_effect_value }, { N_("gray" ), dt_action_effect_value }, { N_("white"), dt_action_effect_value }, { NULL } }; static float _action_process(gpointer target, const dt_action_element_t element, const dt_action_effect_t effect, float move_size) { if(element >= DT_IOP_RGBLEVELS_MAX_CHANNELS) return DT_ACTION_NOT_VALID; dt_iop_module_t *self = g_object_get_data(G_OBJECT(target), "iop-instance"); dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_rgblevels_params_t *p = self->params; if(DT_PERFORM_ACTION(move_size)) { float bottop = -1e6; switch(effect) { case DT_ACTION_EFFECT_RESET: p->levels[g->channel][0] = RGBLEVELS_MIN; p->levels[g->channel][1] = RGBLEVELS_MID; p->levels[g->channel][2] = RGBLEVELS_MAX; gtk_widget_queue_draw(target); break; case DT_ACTION_EFFECT_BOTTOM: bottop *= -1; case DT_ACTION_EFFECT_TOP: move_size = bottop; case DT_ACTION_EFFECT_DOWN: move_size *= -1; case DT_ACTION_EFFECT_UP: g->drag_start_percentage = (p->levels[g->channel][1] - p->levels[g->channel][0]) / (p->levels[g->channel][2] - p->levels[g->channel][0]); const float interval = 0.02; // Distance moved for each scroll event const float new_position = p->levels[g->channel][element] + interval * move_size; _rgblevels_move_handle(self, element, new_position, p->levels[g->channel], g->drag_start_percentage); default: dt_print(DT_DEBUG_ALWAYS, "[_action_process_tabs] unknown shortcut effect (%d) for levels", effect); break; } gchar *text = g_strdup_printf("%s %.2f", _action_elements_levels[element].name, p->levels[g->channel][element]); dt_action_widget_toast(DT_ACTION(self), target, text); g_free(text); } return p->levels[g->channel][element]; } const dt_action_def_t _action_def_levels = { N_("levels"), _action_process, _action_elements_levels }; void gui_init(dt_iop_module_t *self) { dt_iop_rgblevels_gui_data_t *g = IOP_GUI_ALLOC(rgblevels); change_image(self); g->mouse_x = g->mouse_y = -1.0; g->dragging = 0; g->last_picked_color = -1; g->cmb_autoscale = dt_bauhaus_combobox_from_params(self, "autoscale"); gtk_widget_set_tooltip_text(g->cmb_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); g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL, 0, "plugins/darkroom/rgblevels/graphheight")); g_object_set_data(G_OBJECT(g->area), "iop-instance", self); dt_action_define_iop(self, NULL, N_("levels"), GTK_WIDGET(g->area), &_action_def_levels); gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("drag handles to set black, gray, and white points. " "operates on L channel.")); g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(_area_draw_callback), self); dt_gui_connect_click(g->area, _area_button_press_callback, _area_button_release_callback, 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_scroll_callback, self); #define PICKER_SETUP(color, name, tooltip) \ g->color##pick = dt_color_picker_new(self, DT_COLOR_PICKER_POINT, NULL); \ dt_action_define_iop(self, N_("pickers"), name, g->color##pick, \ &dt_action_def_color_picker); \ gtk_widget_set_tooltip_text(g->color##pick, tooltip); \ gtk_widget_set_name(GTK_WIDGET(g->color##pick), "picker-"#color); \ g_signal_connect(dt_gui_expand(g->color##pick), "toggled", \ G_CALLBACK(_color_picker_callback), self); PICKER_SETUP(black, N_("black"), _("pick black point from image")); PICKER_SETUP(grey, N_("gray"), _("pick medium gray point from image")); PICKER_SETUP(white, N_("white"), _("pick white point from image")); g->bt_auto_levels = gtk_button_new_with_label(_("auto")); dt_action_define_iop(self, NULL, N_("auto levels"), g->bt_auto_levels, &dt_action_def_button); gtk_widget_set_tooltip_text(g->bt_auto_levels, _("apply auto levels")); g->bt_select_region = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_colorpicker, 0, NULL, &(dtgtk_button_config_t){ .tooltip = _("apply auto levels based on a region defined by the user\n" "click and drag to draw the area\n" "right-click to cancel"), .action = DT_ACTION(self), .action_label = N_("auto region"), .action_def = &dt_action_def_toggle, }); dt_gui_add_class(g->bt_select_region, "dt_transparent_background"); dt_gui_box_add(self->widget, g->channel_tabs, g->area, dt_gui_hbox(g->blackpick, g->greypick, g->whitepick), dt_gui_hbox(dt_gui_expand(g->bt_auto_levels), dt_gui_expand(g->bt_select_region))); g_signal_connect(G_OBJECT(g->bt_auto_levels), "clicked", G_CALLBACK(_auto_levels_callback), self); g_signal_connect(G_OBJECT(g->bt_select_region), "toggled", G_CALLBACK(_select_region_toggled_callback), self); 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")); // add signal handler for preview pipe finish DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _develop_ui_pipe_finished_callback); } static void _get_selected_area(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, dt_iop_rgblevels_gui_data_t *g, const dt_iop_roi_t *const roi_in, int *box_out) { box_out[0] = box_out[1] = box_out[2] = box_out[3] = 0; if(g) { const int width = roi_in->width; const int height = roi_in->height; dt_boundingbox_t box_cood = { g->box_cood[0], g->box_cood[1], g->box_cood[2], g->box_cood[3] }; box_cood[0] *= piece->pipe->iwidth; box_cood[1] *= piece->pipe->iheight; box_cood[2] *= piece->pipe->iwidth; box_cood[3] *= piece->pipe->iheight; dt_dev_distort_transform_plus(self->dev, piece->pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_INCL, box_cood, 2); box_cood[0] *= roi_in->scale; box_cood[1] *= roi_in->scale; box_cood[2] *= roi_in->scale; box_cood[3] *= roi_in->scale; box_cood[0] -= roi_in->x; box_cood[1] -= roi_in->y; box_cood[2] -= roi_in->x; box_cood[3] -= roi_in->y; int DT_ALIGNED_ARRAY box[4]; // re-order edges of bounding box box[0] = fminf(box_cood[0], box_cood[2]); box[1] = fminf(box_cood[1], box_cood[3]); box[2] = fmaxf(box_cood[0], box_cood[2]); box[3] = fmaxf(box_cood[1], box_cood[3]); // do not continue if box is completely outside of roi if(!(box[0] >= width || box[1] >= height || box[2] < 0 || box[3] < 0)) { // clamp bounding box to roi for(int k = 0; k < 4; k += 2) box[k] = MIN(width - 1, MAX(0, box[k])); for(int k = 1; k < 4; k += 2) box[k] = MIN(height - 1, MAX(0, box[k])); // safety check: area needs to have minimum 1 pixel width and height if(!(box[2] - box[0] < 1 || box[3] - box[1] < 1)) { box_out[0] = box[0]; box_out[1] = box[1]; box_out[2] = box[2]; box_out[3] = box[3]; } } } } static void _auto_levels(const float *const img, const int width, const int height, int *box_area, dt_iop_rgblevels_params_t *p, const int _channel, const dt_iop_order_iccprofile_info_t *const work_profile) { int y_from, y_to, x_from, x_to; const int ch = 4; const int channel = (p->autoscale == DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS) ? _channel : 0; if(box_area[2] > box_area[0] && box_area[3] > box_area[1]) { y_from = box_area[1]; y_to = box_area[3]; x_from = box_area[0]; x_to = box_area[2]; } else { y_from = 0; y_to = height - 1; x_from = 0; x_to = width - 1; } float max = -FLT_MAX; float min = FLT_MAX; for(int y = y_from; y <= y_to; y++) { const float *const in = img + ch * width * y; for(int x = x_from; x <= x_to; x++) { const float *const pixel = in + x * ch; if(p->autoscale == DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS || p->preserve_colors == DT_RGB_NORM_NONE) { if(p->autoscale == DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS) { if(pixel[channel] >= 0.f) { max = fmaxf(max, pixel[channel]); min = fminf(min, pixel[channel]); } } else { for(int c = 0; c < 3; c++) { if(pixel[c] >= 0.f) { max = fmaxf(max, pixel[c]); min = fminf(min, pixel[c]); } } } } else { const float lum = dt_rgb_norm(pixel, p->preserve_colors, work_profile); if(lum >= 0.f) { max = fmaxf(max, lum); min = fminf(min, lum); } } } } p->levels[channel][0] = CLAMP(min, 0.f, 1.f); p->levels[channel][2] = CLAMP(max, 0.f, 1.f); p->levels[channel][1] = (p->levels[channel][2] + p->levels[channel][0]) / 2.f; } void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, ivoid, ovoid, roi_in, roi_out)) return; // image has been copied through to output and module's // trouble flag has been updated const dt_iop_rgblevels_data_t *const d = (dt_iop_rgblevels_data_t *)piece->data; dt_iop_rgblevels_params_t *p = (dt_iop_rgblevels_params_t *)&d->params; dt_iop_rgblevels_gui_data_t *g = self->gui_data; const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(piece->pipe); // process auto levels if(g && dt_pipe_is_preview(piece->pipe)) { dt_iop_gui_enter_critical_section(self); if(g->call_auto_levels == 1 && !DT_IN_GUI_UPDATE()) { g->call_auto_levels = -1; dt_iop_gui_leave_critical_section(self); memcpy(&g->params, p, sizeof(dt_iop_rgblevels_params_t)); int box[4] = { 0 }; _get_selected_area(self, piece, g, roi_in, box); _auto_levels((const float *const)ivoid, roi_in->width, roi_in->height, box, &(g->params), g->channel, work_profile); dt_iop_gui_enter_critical_section(self); g->call_auto_levels = 2; dt_iop_gui_leave_critical_section(self); } else { dt_iop_gui_leave_critical_section(self); } } const dt_aligned_pixel_t mult = { 1.f / (d->params.levels[0][2] - d->params.levels[0][0]), 1.f / (d->params.levels[1][2] - d->params.levels[1][0]), 1.f / (d->params.levels[2][2] - d->params.levels[2][0]) }; const size_t npixels = (size_t)roi_out->width * roi_out->height; const float *const restrict in = (const float*)ivoid; float *const restrict out = (float*)ovoid; if(d->params.autoscale == DT_IOP_RGBLEVELS_INDEPENDENT_CHANNELS || d->params.preserve_colors == DT_RGB_NORM_NONE) { const dt_aligned_pixel_t min_levels = { d->params.levels[0][0], d->params.levels[1][0], d->params.levels[2][0], 0.0f }; const dt_aligned_pixel_t max_levels = { d->params.levels[0][2], d->params.levels[1][2], d->params.levels[2][2], 1.0f }; DT_OMP_FOR() for(size_t k = 0; k < 4U*npixels; k += 4) { for(int c = 0; c < 3; c++) { const float L_in = in[k+c]; if(L_in <= min_levels[c]) { // Anything below the lower threshold just clips to zero out[k+c] = 0.0f; } else if(L_in >= max_levels[c]) { // above the upper limit we extrapolate using the gamma value const float percentage = (L_in - min_levels[c]) * mult[c]; out[k+c] = powf(percentage, d->inv_gamma[c]); } else { // Within the expected input range we can use the lookup table const float percentage = (L_in - min_levels[c]) * mult[c]; out[k+c] = d->lut[c][CLAMP((int)(percentage * 0x10000ul), 0, 0xffff)]; } } } } else { const int ch_levels = 0; const float mult_ch = mult[ch_levels]; const float *const restrict levels = d->params.levels[ch_levels]; const float min_level = levels[0]; const float max_level = levels[2]; static const dt_aligned_pixel_t zero = { 0.0f, 0.0f, 0.0f, 0.0f }; DT_OMP_FOR() for(size_t k = 0; k < 4U*npixels; k += 4) { const float lum = dt_rgb_norm(in+k, d->params.preserve_colors, work_profile); if(lum > min_level) { float curve_lum; const float percentage = (lum - min_level) * mult_ch; if(lum >= max_level) { curve_lum = powf(percentage, d->inv_gamma[ch_levels]); } else { // Within the expected input range we can use the lookup table curve_lum = d->lut[ch_levels][CLAMP((int)(percentage * 0x10000ul), 0, 0xffff)]; } const float ratio = curve_lum / lum; dt_aligned_pixel_t res; for_each_channel(c,aligned(in,out:16)) { res[c] = (ratio * in[k+c]); } copy_pixel_nontemporal(out + k, res); } else { copy_pixel_nontemporal(out + k, zero); } } dt_omploop_sfence(); // ensure nontemporal writes are flushed before continuing } } #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); const int ch = piece->colors; dt_iop_rgblevels_data_t *d = piece->data; dt_iop_rgblevels_params_t *p = &d->params; dt_iop_rgblevels_gui_data_t *g = self->gui_data; dt_iop_rgblevels_global_data_t *gd = self->global_data; cl_int err = CL_SUCCESS; float *src_buffer = NULL; cl_mem dev_lutr = NULL; cl_mem dev_lutg = NULL; cl_mem dev_lutb = NULL; cl_mem dev_levels = NULL; cl_mem dev_inv_gamma = NULL; cl_mem dev_profile_info = NULL; cl_mem dev_profile_lut = NULL; dt_colorspaces_iccprofile_info_cl_t *profile_info_cl = NULL; 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_out->width; const int height = roi_out->height; // process auto levels if(g && dt_pipe_is_preview(piece->pipe)) { dt_iop_gui_enter_critical_section(self); if(g->call_auto_levels == 1 && !DT_IN_GUI_UPDATE()) { g->call_auto_levels = -1; dt_iop_gui_leave_critical_section(self); // get the image, this works only in C src_buffer = dt_alloc_align_float((size_t)ch * width * height); if(src_buffer == NULL) { dt_print(DT_DEBUG_ALWAYS, "[rgblevels process_cl] error allocating memory for temp table 1"); err = CL_MEM_OBJECT_ALLOCATION_FAILURE; goto cleanup; } err = dt_opencl_copy_image_to_host(devid, src_buffer, dev_in, width, height, ch * sizeof(float)); if(err != CL_SUCCESS) { dt_print(DT_DEBUG_ALWAYS, "[rgblevels process_cl] error allocating memory for temp table 2"); goto cleanup; } memcpy(&g->params, p, sizeof(dt_iop_rgblevels_params_t)); int box[4] = { 0 }; _get_selected_area(self, piece, g, roi_in, box); _auto_levels(src_buffer, roi_in->width, roi_in->height, box, &(g->params), g->channel, work_profile); dt_free_align(src_buffer); src_buffer = NULL; dt_iop_gui_enter_critical_section(self); g->call_auto_levels = 2; dt_iop_gui_leave_critical_section(self); } else { dt_iop_gui_leave_critical_section(self); } } const int autoscale = d->params.autoscale; const int preserve_colors = d->params.preserve_colors; dev_lutr = dt_opencl_copy_host_to_image(devid, d->lut[0], 256, 256, sizeof(float)); if(dev_lutr == NULL) { err = CL_MEM_OBJECT_ALLOCATION_FAILURE; goto cleanup; } dev_lutg = dt_opencl_copy_host_to_image(devid, d->lut[1], 256, 256, sizeof(float)); if(dev_lutg == NULL) { err = CL_MEM_OBJECT_ALLOCATION_FAILURE; goto cleanup; } dev_lutb = dt_opencl_copy_host_to_image(devid, d->lut[2], 256, 256, sizeof(float)); if(dev_lutb == NULL) { err = CL_MEM_OBJECT_ALLOCATION_FAILURE; goto cleanup; } dev_levels = dt_opencl_copy_host_to_device_constant (devid, sizeof(float) * 3 * 3, (float *)d->params.levels); if(dev_levels == NULL) { err = CL_MEM_OBJECT_ALLOCATION_FAILURE; goto cleanup; } dev_inv_gamma = dt_opencl_copy_host_to_device_constant (devid, sizeof(float) * 3, (float *)d->inv_gamma); if(dev_inv_gamma == NULL) { err = CL_MEM_OBJECT_ALLOCATION_FAILURE; goto cleanup; } 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 = dt_opencl_enqueue_kernel_2d_args (devid, gd->kernel_levels, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(autoscale), CLARG(preserve_colors), CLARG(dev_lutr), CLARG(dev_lutg), CLARG(dev_lutb), CLARG(dev_levels), CLARG(dev_inv_gamma), CLARG(dev_profile_info), CLARG(dev_profile_lut), CLARG(use_work_profile)); cleanup: dt_opencl_release_mem_object(dev_lutr); dt_opencl_release_mem_object(dev_lutg); dt_opencl_release_mem_object(dev_lutb); dt_opencl_release_mem_object(dev_levels); dt_opencl_release_mem_object(dev_inv_gamma); dt_ioppr_free_iccprofile_params_cl(&profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut); dt_free_align(src_buffer); return err; } #endif // 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