1 /*
2     This file is part of darktable,
3     Copyright (C) 2020 darktable developers.
4 
5     darktable is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     darktable is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 #ifdef HAVE_CONFIG_H
19 #include "config.h"
20 #endif
21 
22 #include "bauhaus/bauhaus.h"
23 #include "common/darktable.h"
24 #include "common/opencl.h"
25 #include "control/control.h"
26 #include "develop/develop.h"
27 #include "develop/imageop.h"
28 #include "develop/imageop_math.h"
29 #include "develop/imageop_gui.h"
30 #include "develop/openmp_maths.h"
31 #include "dtgtk/button.h"
32 #include "dtgtk/resetlabel.h"
33 #include "gui/accelerators.h"
34 #include "gui/gtk.h"
35 #include "gui/presets.h"
36 #include "gui/color_picker_proxy.h"
37 #include "iop/iop_api.h"
38 
39 #include <glib.h>
40 #include <math.h>
41 #include <stdlib.h>
42 
43 #if defined(__GNUC__)
44 #pragma GCC optimize ("unroll-loops", "tree-loop-if-convert", \
45                       "tree-loop-distribution", "no-strict-aliasing", \
46                       "loop-interchange",  "tree-loop-im", \
47                       "unswitch-loops", "tree-loop-ivcanon", "ira-loop-pressure", \
48                       "split-ivs-in-unroller", "variable-expansion-in-unroller", \
49                       "split-loops", "ivopts", "predictive-commoning",\
50                          \
51                       "finite-math-only", "fp-contract=fast", "fast-math")
52 #endif
53 
54 /** DOCUMENTATION
55  *
56  * This module allows to invert scanned negatives and simulate their print on paper, based on Kodak Cineon
57  * densitometry algorithm. It is better than the old invert module because it takes into account the Dmax of the film
58  * and allows white balance adjustments, as well as paper grade (gamma) simulation. It also allows density correction
59  * in log space, to account for the exposure settings of the scanner. Finally, it is applied after input colour profiling,
60  * which means the inversion happens after the scanner or the camera got color-corrected, while the old invert module
61  * invert the RAW, non-demosaiced, file before any colour correction.
62  *
63  * References :
64  *
65  *  - https://www.kodak.com/uploadedfiles/motion/US_plugins_acrobat_en_motion_education_sensitometry_workbook.pdf
66  *  - http://www.digital-intermediate.co.uk/film/pdf/Cineon.pdf
67  *  - https://lists.gnu.org/archive/html/openexr-devel/2005-03/msg00009.html
68  **/
69 
70  #define THRESHOLD 2.3283064365386963e-10f // -32 EV
71 
72 
73 DT_MODULE_INTROSPECTION(2, dt_iop_negadoctor_params_t)
74 
75 
76 typedef enum dt_iop_negadoctor_filmstock_t
77 {
78   // What kind of emulsion are we working on ?
79   DT_FILMSTOCK_NB = 0,   // $DESCRIPTION: "black and white film"
80   DT_FILMSTOCK_COLOR = 1 // $DESCRIPTION: "color film"
81 } dt_iop_negadoctor_filmstock_t;
82 
83 
84 typedef struct dt_iop_negadoctor_params_t
85 {
86   dt_iop_negadoctor_filmstock_t film_stock; /* $DEFAULT: DT_FILMSTOCK_COLOR $DESCRIPTION: "film stock" */
87   float Dmin[4];                            /* color of film substrate
88                                                $MIN: 0.00001 $MAX: 1.5 $DEFAULT: 1.0 */
89   float wb_high[4];                         /* white balance RGB coeffs (illuminant)
90                                                $MIN: 0.25 $MAX: 2 $DEFAULT: 1.0 */
91   float wb_low[4];                          /* white balance RGB offsets (base light)
92                                                $MIN: 0.25 $MAX: 2 $DEFAULT: 1.0 */
93   float D_max;                              /* max density of film
94                                                $MIN: 0.1 $MAX: 6 $DEFAULT: 2.046 */
95   float offset;                             /* inversion offset
96                                                $MIN: -1.0 $MAX: 1.0 $DEFAULT: -0.05 $DESCRIPTION: "scan exposure bias" */
97   float black;                              /* display black level
98                                                $MIN: -0.5 $MAX: 0.5 $DEFAULT: 0.0755 $DESCRIPTION: "paper black (density correction)" */
99   float gamma;                              /* display gamma
100                                                $MIN: 1.0 $MAX: 8.0 $DEFAULT: 4.0 $DESCRIPTION: "paper grade (gamma)" */
101   float soft_clip;                          /* highlights roll-off
102                                                $MIN: 0.0001 $MAX: 1.0 $DEFAULT: 0.75 $DESCRIPTION: "paper gloss (specular highlights)" */
103   float exposure;                           /* extra exposure
104                                                $MIN: 0.5 $MAX: 2.0 $DEFAULT: 0.9245 $DESCRIPTION: "print exposure adjustment" */
105 } dt_iop_negadoctor_params_t;
106 
107 
108 typedef struct dt_iop_negadoctor_data_t
109 {
110   dt_aligned_pixel_t Dmin;                // color of film substrate
111   dt_aligned_pixel_t wb_high;             // white balance RGB coeffs / Dmax
112   dt_aligned_pixel_t offset;              // inversion offset
113   float black;                            // display black level
114   float gamma;                            // display gamma
115   float soft_clip;                        // highlights roll-off
116   float soft_clip_comp;                   // 1 - softclip, complement to 1
117   float exposure;                         // extra exposure
118 } dt_iop_negadoctor_data_t;
119 
120 
121 typedef struct dt_iop_negadoctor_gui_data_t
122 {
123   GtkNotebook *notebook;
124   GtkWidget *film_stock;
125   GtkWidget *Dmin_R, *Dmin_G, *Dmin_B;
126   GtkWidget *wb_high_R, *wb_high_G, *wb_high_B;
127   GtkWidget *wb_low_R, *wb_low_G, *wb_low_B;
128   GtkWidget *D_max;
129   GtkWidget *offset;
130   GtkWidget *black, *gamma, *soft_clip, *exposure;
131   GtkWidget *Dmin_picker, *Dmin_sampler;
132   GtkWidget *WB_high_picker, *WB_high_sampler;
133   GtkWidget *WB_low_picker, *WB_low_sampler;
134 } dt_iop_negadoctor_gui_data_t;
135 
136 
137 typedef struct dt_iop_negadoctor_global_data_t
138 {
139   int kernel_negadoctor;
140 } dt_iop_negadoctor_global_data_t;
141 
142 
name()143 const char *name()
144 {
145   return _("negadoctor");
146 }
147 
aliases()148 const char *aliases()
149 {
150   return _("film|invert|negative|scan");
151 }
152 
description(struct dt_iop_module_t * self)153 const char *description(struct dt_iop_module_t *self)
154 {
155   return dt_iop_set_description(self, _("invert film negative scans and simulate printing on paper"),
156                                       _("corrective and creative"),
157                                       _("linear, RGB, display-referred"),
158                                       _("non-linear, RGB"),
159                                       _("non-linear, RGB, display-referred"));
160 }
161 
flags()162 int flags()
163 {
164   return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_ONE_INSTANCE;
165 }
166 
167 
default_group()168 int default_group()
169 {
170   return IOP_GROUP_BASIC | IOP_GROUP_TECHNICAL;
171 }
172 
173 
default_colorspace(dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)174 int default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
175 {
176   return iop_cs_rgb;
177 }
178 
legacy_params(dt_iop_module_t * self,const void * const old_params,const int old_version,void * new_params,const int new_version)179 int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version,
180                   void *new_params, const int new_version)
181 {
182   if(old_version == 1 && new_version == 2)
183   {
184     typedef struct dt_iop_negadoctor_params_v1_t
185     {
186       dt_iop_negadoctor_filmstock_t film_stock;
187       dt_aligned_pixel_t Dmin;                // color of film substrate
188       dt_aligned_pixel_t wb_high;             // white balance RGB coeffs (illuminant)
189       dt_aligned_pixel_t wb_low;              // white balance RGB offsets (base light)
190       float D_max;                            // max density of film
191       float offset;                           // inversion offset
192       float black;                            // display black level
193       float gamma;                            // display gamma
194       float soft_clip;                        // highlights roll-off
195       float exposure;                         // extra exposure
196     } dt_iop_negadoctor_params_v1_t;
197 
198     dt_iop_negadoctor_params_v1_t *o = (dt_iop_negadoctor_params_v1_t *)old_params;
199     dt_iop_negadoctor_params_t *n = (dt_iop_negadoctor_params_t *)new_params;
200     dt_iop_negadoctor_params_t *d = (dt_iop_negadoctor_params_t *)self->default_params;
201 
202     *n = *d; // start with a fresh copy of default parameters
203 
204     // WARNING: when copying the arrays in a for loop, gcc wrongly assumed
205     //          that n and o were aligned and used AVX instructions for me,
206     //          which segfaulted. let's hope this doesn't get optimized too much.
207     n->film_stock = o->film_stock;
208     n->Dmin[0] = o->Dmin[0];
209     n->Dmin[1] = o->Dmin[1];
210     n->Dmin[2] = o->Dmin[2];
211     n->Dmin[3] = o->Dmin[3];
212     n->wb_high[0] = o->wb_high[0];
213     n->wb_high[1] = o->wb_high[1];
214     n->wb_high[2] = o->wb_high[2];
215     n->wb_high[3] = o->wb_high[3];
216     n->wb_low[0] = o->wb_low[0];
217     n->wb_low[1] = o->wb_low[1];
218     n->wb_low[2] = o->wb_low[2];
219     n->wb_low[3] = o->wb_low[3];
220     n->D_max = o->D_max;
221     n->offset = o->offset;
222     n->black = o->black;
223     n->gamma = o->gamma;
224     n->soft_clip = o->soft_clip;
225     n->exposure = o->exposure;
226 
227     return 0;
228   }
229   return 1;
230 }
231 
commit_params(dt_iop_module_t * self,dt_iop_params_t * p1,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)232 void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
233                    dt_dev_pixelpipe_iop_t *piece)
234 {
235   const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)p1;
236   dt_iop_negadoctor_data_t *const d = (dt_iop_negadoctor_data_t *)piece->data;
237 
238   // keep WB_high even in B&W mode to apply sepia or warm tone look
239   // but premultiply it aheard with Dmax to spare one div per pixel
240   for(size_t c = 0; c < 4; c++) d->wb_high[c] = p->wb_high[c] / p->D_max;
241 
242   for(size_t c = 0; c < 4; c++) d->offset[c] = p->wb_high[c] * p->offset * p->wb_low[c];
243 
244   // ensure we use a monochrome Dmin for B&W film
245   if(p->film_stock == DT_FILMSTOCK_COLOR)
246     for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[c];
247   else if(p->film_stock == DT_FILMSTOCK_NB)
248     for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[0];
249 
250   // arithmetic trick allowing to rewrite the pixel inversion as FMA
251   d->black = -p->exposure * (1.0f + p->black);
252 
253   // highlights soft clip
254   d->soft_clip = p->soft_clip;
255   d->soft_clip_comp = 1.0f - p->soft_clip;
256 
257   // copy
258   d->exposure = p->exposure;
259   d->gamma = p->gamma;
260 }
261 
262 
process(struct dt_iop_module_t * const self,dt_dev_pixelpipe_iop_t * const piece,const void * const restrict ivoid,void * const restrict ovoid,const dt_iop_roi_t * const restrict roi_in,const dt_iop_roi_t * const restrict roi_out)263 void process(struct dt_iop_module_t *const self, dt_dev_pixelpipe_iop_t *const piece,
264              const void *const restrict ivoid, void *const restrict ovoid,
265              const dt_iop_roi_t *const restrict roi_in, const dt_iop_roi_t *const restrict roi_out)
266 {
267   const dt_iop_negadoctor_data_t *const d = piece->data;
268   assert(piece->colors = 4);
269 
270   const float *const restrict in = (float *)ivoid;
271   float *const restrict out = (float *)ovoid;
272 
273 
274 #ifdef _OPENMP
275   #pragma omp parallel for simd default(none) \
276     dt_omp_firstprivate(d, in, out, roi_out) \
277     aligned(in, out:64) collapse(2)
278 #endif
279   for(size_t k = 0; k < (size_t)roi_out->height * roi_out->width * 4; k += 4)
280   {
281     for(size_t c = 0; c < 4; c++)
282     {
283       // Unpack vectors one by one with extra pragmas to be sure the compiler understands they can be vectorized
284       const float *const restrict pix_in = in + k;
285       float *const restrict pix_out = out + k;
286       const float *const restrict Dmin = __builtin_assume_aligned(d->Dmin, 16);
287       const float *const restrict wb_high = __builtin_assume_aligned(d->wb_high, 16);
288       const float *const restrict offset = __builtin_assume_aligned(d->offset, 16);
289 
290       // Convert transmission to density using Dmin as a fulcrum
291       const float density = - log10f(Dmin[c] / fmaxf(pix_in[c], THRESHOLD)); // threshold to -32 EV
292 
293       // Correct density in log space
294       const float corrected_de = wb_high[c] * density + offset[c];
295 
296       // Print density on paper : ((1 - 10^corrected_de + black) * exposure)^gamma rewritten for FMA
297       const float print_linear = -(d->exposure * fast_exp10f(corrected_de) + d->black);
298       const float print_gamma = powf(fmaxf(print_linear, 0.0f), d->gamma); // note : this is always > 0
299 
300       // Compress highlights. from https://lists.gnu.org/archive/html/openexr-devel/2005-03/msg00009.html
301       pix_out[c] =  (print_gamma > d->soft_clip) ? d->soft_clip + (1.0f - fast_expf(-(print_gamma - d->soft_clip) / d->soft_clip_comp)) * d->soft_clip_comp
302                                                  : print_gamma;
303     }
304   }
305 
306   if(piece->pipe->mask_display & DT_DEV_PIXELPIPE_DISPLAY_MASK)
307     dt_iop_alpha_copy(ivoid, ovoid, roi_out->width, roi_out->height);
308 }
309 
310 
311 #ifdef HAVE_OPENCL
process_cl(struct dt_iop_module_t * const self,dt_dev_pixelpipe_iop_t * const piece,cl_mem dev_in,cl_mem dev_out,const dt_iop_roi_t * const restrict roi_in,const dt_iop_roi_t * const restrict roi_out)312 int process_cl(struct dt_iop_module_t *const self, dt_dev_pixelpipe_iop_t *const piece, cl_mem dev_in, cl_mem dev_out,
313                const dt_iop_roi_t *const restrict roi_in, const dt_iop_roi_t *const restrict roi_out)
314 {
315   const dt_iop_negadoctor_data_t *const d = (dt_iop_negadoctor_data_t *)piece->data;
316   const dt_iop_negadoctor_global_data_t *const gd = (dt_iop_negadoctor_global_data_t *)self->global_data;
317 
318   cl_int err = -999;
319 
320   const int devid = piece->pipe->devid;
321   const int width = roi_in->width;
322   const int height = roi_in->height;
323 
324   size_t sizes[] = { ROUNDUPWD(width), ROUNDUPHT(height), 1 };
325 
326   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 0, sizeof(cl_mem), (void *)&dev_in);
327   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 1, sizeof(cl_mem), (void *)&dev_out);
328   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 2, sizeof(int), (void *)&width);
329   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 3, sizeof(int), (void *)&height);
330   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 4, 4 * sizeof(float), (void *)&d->Dmin);
331   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 5, 4 * sizeof(float), (void *)&d->wb_high);
332   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 6, 4 * sizeof(float), (void *)&d->offset);
333   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 7, sizeof(float), (void *)&d->exposure);
334   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 8, sizeof(float), (void *)&d->black);
335   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 9, sizeof(float), (void *)&d->gamma);
336   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 10, sizeof(float), (void *)&d->soft_clip);
337   dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 11, sizeof(float), (void *)&d->soft_clip_comp);
338 
339   err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_negadoctor, sizes);
340   if(err != CL_SUCCESS) goto error;
341     return TRUE;
342 
343 error:
344   dt_print(DT_DEBUG_OPENCL, "[opencl_negadoctor] couldn't enqueue kernel! %d\n", err);
345   return FALSE;
346 }
347 #endif
348 
349 
init(dt_iop_module_t * module)350 void init(dt_iop_module_t *module)
351 {
352   dt_iop_default_init(module);
353 
354   dt_iop_negadoctor_params_t *d = module->default_params;
355 
356   d->Dmin[0] = 1.00f;
357   d->Dmin[1] = 0.45f;
358   d->Dmin[2] = 0.25f;
359 }
360 
init_presets(dt_iop_module_so_t * self)361 void init_presets(dt_iop_module_so_t *self)
362 {
363   dt_iop_negadoctor_params_t tmp = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_COLOR,
364                                                                  .Dmin = { 1.13f, 0.49f, 0.27f, 0.0f},
365                                                                  .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f },
366                                                                  .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f },
367                                                                  .D_max = 1.6f,
368                                                                  .offset = -0.05f,
369                                                                  .gamma = 4.0f,
370                                                                  .soft_clip = 0.75f,
371                                                                  .exposure = 0.9245f,
372                                                                  .black = 0.0755f };
373 
374 
375   dt_gui_presets_add_generic(_("color film"), self->op,
376                              self->version(), &tmp, sizeof(tmp), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
377 
378   dt_iop_negadoctor_params_t tmq = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_NB,
379                                                                  .Dmin = { 1.0f, 1.0f, 1.0f, 0.0f},
380                                                                  .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f },
381                                                                  .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f },
382                                                                  .D_max = 2.2f,
383                                                                  .offset = -0.05f,
384                                                                  .gamma = 5.0f,
385                                                                  .soft_clip = 0.75f,
386                                                                  .exposure = 1.f,
387                                                                  .black = 0.0755f };
388 
389 
390   dt_gui_presets_add_generic(_("black and white film"), self->op,
391                              self->version(), &tmq, sizeof(tmq), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
392 }
393 
init_global(dt_iop_module_so_t * module)394 void init_global(dt_iop_module_so_t *module)
395 {
396   dt_iop_negadoctor_global_data_t *gd
397       = (dt_iop_negadoctor_global_data_t *)malloc(sizeof(dt_iop_negadoctor_global_data_t));
398 
399   module->data = gd;
400   const int program = 30; // negadoctor.cl, from programs.conf
401   gd->kernel_negadoctor = dt_opencl_create_kernel(program, "negadoctor");
402 }
403 
cleanup_global(dt_iop_module_so_t * module)404 void cleanup_global(dt_iop_module_so_t *module)
405 {
406   dt_iop_negadoctor_global_data_t *gd = module->data;
407   dt_opencl_free_kernel(gd->kernel_negadoctor);
408   free(module->data);
409   module->data = NULL;
410 }
411 
init_pipe(struct dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)412 void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
413 {
414   piece->data = g_malloc0(sizeof(dt_iop_negadoctor_data_t));
415 }
416 
cleanup_pipe(struct dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)417 void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
418 {
419   g_free(piece->data);
420   piece->data = NULL;
421 }
422 
423 
424 /* Global GUI stuff */
425 
setup_color_variables(dt_iop_negadoctor_gui_data_t * const g,const gint state)426 static void setup_color_variables(dt_iop_negadoctor_gui_data_t *const g, const gint state)
427 {
428   gtk_widget_set_visible(g->Dmin_G, state);
429   gtk_widget_set_visible(g->Dmin_B, state);
430 }
431 
432 
toggle_stock_controls(dt_iop_module_t * const self)433 static void toggle_stock_controls(dt_iop_module_t *const self)
434 {
435   dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
436   const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)self->params;
437 
438   if(p->film_stock == DT_FILMSTOCK_NB)
439   {
440     // Hide color controls
441     setup_color_variables(g, FALSE);
442     dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min"));
443   }
444   else if(p->film_stock == DT_FILMSTOCK_COLOR)
445   {
446     // Show color controls
447     setup_color_variables(g, TRUE);
448     dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min red component"));
449   }
450   else
451   {
452     // We shouldn't be there
453     fprintf(stderr, "negadoctor film stock: undefined behaviour\n");
454   }
455 }
456 
457 
Dmin_picker_update(dt_iop_module_t * self)458 static void Dmin_picker_update(dt_iop_module_t *self)
459 {
460   dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
461   const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)self->params;
462 
463   GdkRGBA color;
464   color.alpha = 1.0f;
465 
466   if(p->film_stock == DT_FILMSTOCK_COLOR)
467   {
468     color.red = p->Dmin[0];
469     color.green = p->Dmin[1];
470     color.blue = p->Dmin[2];
471   }
472   else if(p->film_stock == DT_FILMSTOCK_NB)
473   {
474     color.red = color.green = color.blue = p->Dmin[0];
475   }
476 
477   gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->Dmin_picker), &color);
478 }
479 
Dmin_picker_callback(GtkColorButton * widget,dt_iop_module_t * self)480 static void Dmin_picker_callback(GtkColorButton *widget, dt_iop_module_t *self)
481 {
482   if(darktable.gui->reset) return;
483   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
484   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
485 
486   dt_iop_color_picker_reset(self, TRUE);
487 
488   GdkRGBA c;
489   gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c);
490   p->Dmin[0] = c.red;
491   p->Dmin[1] = c.green;
492   p->Dmin[2] = c.blue;
493 
494   ++darktable.gui->reset;
495   dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]);
496   dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]);
497   dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]);
498   --darktable.gui->reset;
499 
500   Dmin_picker_update(self);
501   dt_iop_color_picker_reset(self, TRUE);
502   dt_dev_add_history_item(darktable.develop, self, TRUE);
503 }
504 
WB_low_picker_update(dt_iop_module_t * self)505 static void WB_low_picker_update(dt_iop_module_t *self)
506 {
507   dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
508   const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)self->params;
509 
510   GdkRGBA color;
511   color.alpha = 1.0f;
512 
513   dt_aligned_pixel_t WB_low_invert;
514   for(size_t c = 0; c < 3; ++c) WB_low_invert[c] = 2.0f - p->wb_low[c];
515   const float WB_low_max = v_maxf(WB_low_invert);
516   for(size_t c = 0; c < 3; ++c) WB_low_invert[c] /= WB_low_max;
517 
518   color.red = WB_low_invert[0];
519   color.green = WB_low_invert[1];
520   color.blue = WB_low_invert[2];
521 
522   gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->WB_low_picker), &color);
523 }
524 
WB_low_picker_callback(GtkColorButton * widget,dt_iop_module_t * self)525 static void WB_low_picker_callback(GtkColorButton *widget, dt_iop_module_t *self)
526 {
527   if(darktable.gui->reset) return;
528   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
529   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
530 
531   dt_iop_color_picker_reset(self, TRUE);
532 
533   GdkRGBA c;
534   gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c);
535 
536   dt_aligned_pixel_t RGB = { 2.0f - c.red, 2.0f - c.green, 2.0f - c.blue };
537 
538   float RGB_min = v_minf(RGB);
539   for(size_t k = 0; k < 3; k++) p->wb_low[k] = RGB[k] / RGB_min;
540 
541   ++darktable.gui->reset;
542   dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]);
543   dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]);
544   dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]);
545   --darktable.gui->reset;
546 
547   WB_low_picker_update(self);
548   dt_iop_color_picker_reset(self, TRUE);
549   dt_dev_add_history_item(darktable.develop, self, TRUE);
550 }
551 
552 
WB_high_picker_update(dt_iop_module_t * self)553 static void WB_high_picker_update(dt_iop_module_t *self)
554 {
555   dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
556   const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)self->params;
557 
558   GdkRGBA color;
559   color.alpha = 1.0f;
560 
561   dt_aligned_pixel_t WB_high_invert;
562   for(size_t c = 0; c < 3; ++c) WB_high_invert[c] = 2.0f - p->wb_high[c];
563   const float WB_high_max = v_maxf(WB_high_invert);
564   for(size_t c = 0; c < 3; ++c) WB_high_invert[c] /= WB_high_max;
565 
566   color.red = WB_high_invert[0];
567   color.green = WB_high_invert[1];
568   color.blue = WB_high_invert[2];
569 
570   gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->WB_high_picker), &color);
571 }
572 
WB_high_picker_callback(GtkColorButton * widget,dt_iop_module_t * self)573 static void WB_high_picker_callback(GtkColorButton *widget, dt_iop_module_t *self)
574 {
575   if(darktable.gui->reset) return;
576   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
577   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
578 
579   dt_iop_color_picker_reset(self, TRUE);
580 
581   GdkRGBA c;
582   gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c);
583 
584   dt_aligned_pixel_t RGB = { 2.0f - c.red, 2.0f - c.green, 2.0f - c.blue };
585   float RGB_min = v_minf(RGB);
586   for(size_t k = 0; k < 3; k++) p->wb_high[k] = RGB[k] / RGB_min;
587 
588   ++darktable.gui->reset;
589   dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]);
590   dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]);
591   dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]);
592   --darktable.gui->reset;
593 
594   WB_high_picker_update(self);
595   dt_iop_color_picker_reset(self, TRUE);
596   dt_dev_add_history_item(darktable.develop, self, TRUE);
597 }
598 
599 
600 /* Color pickers auto-tuners */
601 
602 // measure Dmin from the film edges first
apply_auto_Dmin(dt_iop_module_t * self)603 static void apply_auto_Dmin(dt_iop_module_t *self)
604 {
605   if(darktable.gui->reset) return;
606   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
607   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
608 
609   for(int k = 0; k < 4; k++) p->Dmin[k] = self->picked_color[k];
610 
611   ++darktable.gui->reset;
612   dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]);
613   dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]);
614   dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]);
615   --darktable.gui->reset;
616 
617   Dmin_picker_update(self);
618   dt_control_queue_redraw_widget(self->widget);
619   dt_dev_add_history_item(darktable.develop, self, TRUE);
620 }
621 
622 // from Dmin, find out the range of density values of the film and compute Dmax
apply_auto_Dmax(dt_iop_module_t * self)623 static void apply_auto_Dmax(dt_iop_module_t *self)
624 {
625   if(darktable.gui->reset) return;
626   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
627   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
628 
629   dt_aligned_pixel_t RGB;
630   for(int c = 0; c < 3; c++)
631   {
632     RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD));
633   }
634 
635   // Take the max(RGB) for safety. Big values unclip whites
636   p->D_max = v_maxf(RGB);
637 
638   ++darktable.gui->reset;
639   dt_bauhaus_slider_set(g->D_max, p->D_max);
640   --darktable.gui->reset;
641 
642   dt_control_queue_redraw_widget(self->widget);
643   dt_dev_add_history_item(darktable.develop, self, TRUE);
644 }
645 
646 // from Dmax, compute the offset so the range of density is rescaled between [0; 1]
apply_auto_offset(dt_iop_module_t * self)647 static void apply_auto_offset(dt_iop_module_t *self)
648 {
649   if(darktable.gui->reset) return;
650   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
651   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
652 
653   dt_aligned_pixel_t RGB;
654   for(int c = 0; c < 3; c++)
655     RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)) / p->D_max;
656 
657   // Take the min(RGB) for safety. Negative values unclip blacks
658   p->offset = v_minf(RGB);
659 
660   ++darktable.gui->reset;
661   dt_bauhaus_slider_set(g->offset, p->offset);
662   --darktable.gui->reset;
663 
664   dt_control_queue_redraw_widget(self->widget);
665   dt_dev_add_history_item(darktable.develop, self, TRUE);
666 }
667 
668 // from Dmax and offset, compute the white balance correction as multipliers of the offset
669 // such that offset × wb[c] make black monochrome
apply_auto_WB_low(dt_iop_module_t * self)670 static void apply_auto_WB_low(dt_iop_module_t *self)
671 {
672   if(darktable.gui->reset) return;
673   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
674   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
675 
676   dt_aligned_pixel_t RGB_min;
677   for(int c = 0; c < 3; c++)
678     RGB_min[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)) / p->D_max;
679 
680   const float RGB_v_min = v_minf(RGB_min); // warning: can be negative
681   for(int c = 0; c < 3; c++) p->wb_low[c] =  RGB_v_min / RGB_min[c];
682 
683   ++darktable.gui->reset;
684   dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]);
685   dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]);
686   dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]);
687   --darktable.gui->reset;
688 
689   WB_low_picker_update(self);
690   dt_control_queue_redraw_widget(self->widget);
691   dt_dev_add_history_item(darktable.develop, self, TRUE);
692 }
693 
694 // from Dmax, offset and white balance multipliers, compute the white balance of the illuminant as multipliers of 1/Dmax
695 // such that WB[c] / Dmax make white monochrome
apply_auto_WB_high(dt_iop_module_t * self)696 static void apply_auto_WB_high(dt_iop_module_t *self)
697 {
698   if(darktable.gui->reset) return;
699   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
700   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
701 
702   dt_aligned_pixel_t RGB_min;
703   for(int c = 0; c < 3; c++)
704     RGB_min[c] = fabsf(-1.0f / (p->offset * p->wb_low[c] - log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)) / p->D_max));
705 
706   const float RGB_v_min = v_minf(RGB_min); // warning : must be positive
707   for(int c = 0; c < 3; c++) p->wb_high[c] = RGB_min[c] / RGB_v_min;
708 
709   ++darktable.gui->reset;
710   dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]);
711   dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]);
712   dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]);
713   --darktable.gui->reset;
714 
715   WB_high_picker_update(self);
716   dt_control_queue_redraw_widget(self->widget);
717   dt_dev_add_history_item(darktable.develop, self, TRUE);
718 }
719 
720 // from Dmax, offset and both white balances, compute the print black adjustment
721 // such that the printed values range from 0 to + infinity
apply_auto_black(dt_iop_module_t * self)722 static void apply_auto_black(dt_iop_module_t *self)
723 {
724   if(darktable.gui->reset) return;
725   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
726   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
727 
728   dt_aligned_pixel_t RGB;
729   for(int c = 0; c < 3; c++)
730   {
731     RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD));
732     RGB[c] *= p->wb_high[c] / p->D_max;
733     RGB[c] += p->wb_low[c] * p->offset * p->wb_high[c];
734     RGB[c] = 0.1f - (1.0f - fast_exp10f(RGB[c])); // actually, remap between -3.32 EV and infinity for safety because gamma comes later
735   }
736   p->black = v_maxf(RGB);
737 
738   ++darktable.gui->reset;
739   dt_bauhaus_slider_set(g->black, p->black);
740   --darktable.gui->reset;
741 
742   dt_control_queue_redraw_widget(self->widget);
743   dt_dev_add_history_item(darktable.develop, self, TRUE);
744 }
745 
746 // from Dmax, offset, both white balances, and printblack, compute the print exposure adjustment as a scaling factor
747 // such that the printed values range from 0 to 1
apply_auto_exposure(dt_iop_module_t * self)748 static void apply_auto_exposure(dt_iop_module_t *self)
749 {
750   if(darktable.gui->reset) return;
751   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
752   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
753 
754   dt_aligned_pixel_t RGB;
755   for(int c = 0; c < 3; c++)
756   {
757     RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD));
758     RGB[c] *= p->wb_high[c] / p->D_max;
759     RGB[c] += p->wb_low[c] * p->offset;
760     RGB[c] = 0.96f / (1.0f - fast_exp10f(RGB[c]) + p->black); // actually, remap in [0; 0.96] for safety
761   }
762   p->exposure = v_minf(RGB);
763 
764   ++darktable.gui->reset;
765   dt_bauhaus_slider_set(g->exposure, log2f(p->exposure));
766   --darktable.gui->reset;
767 
768   dt_control_queue_redraw_widget(self->widget);
769   dt_dev_add_history_item(darktable.develop, self, TRUE);
770 }
771 
772 
color_picker_apply(dt_iop_module_t * self,GtkWidget * picker,dt_dev_pixelpipe_iop_t * piece)773 void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_iop_t *piece)
774 {
775   if(darktable.gui->reset) return;
776   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
777 
778   if     (picker == g->Dmin_sampler)
779     apply_auto_Dmin(self);
780   else if(picker == g->WB_high_sampler)
781     apply_auto_WB_high(self);
782   else if(picker == g->offset)
783     apply_auto_offset(self);
784   else if(picker == g->D_max)
785     apply_auto_Dmax(self);
786   else if(picker == g->WB_low_sampler)
787     apply_auto_WB_low(self);
788   else if(picker == g->exposure)
789     apply_auto_exposure(self);
790   else if(picker == g->black)
791     apply_auto_black(self);
792   else
793     fprintf(stderr, "[negadoctor] unknown color picker\n");
794 }
795 
gui_init(dt_iop_module_t * self)796 void gui_init(dt_iop_module_t *self)
797 {
798   dt_iop_negadoctor_gui_data_t *g = IOP_GUI_ALLOC(negadoctor);
799 
800   static dt_action_def_t notebook_def = { };
801   g->notebook = dt_ui_notebook_new(&notebook_def);
802   dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), &notebook_def);
803 
804   // Page FILM PROPERTIES
805   GtkWidget *page1 = self->widget = dt_ui_notebook_page(g->notebook, N_("film properties"), NULL);
806 
807   // Dmin
808 
809   gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(_("color of the film base")), FALSE, FALSE, 0);
810 
811   GtkWidget *row1 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
812 
813   g->Dmin_picker = gtk_color_button_new();
814   gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->Dmin_picker), FALSE);
815   gtk_color_button_set_title(GTK_COLOR_BUTTON(g->Dmin_picker), _("select color of film material from a swatch"));
816   gtk_box_pack_start(GTK_BOX(row1), GTK_WIDGET(g->Dmin_picker), TRUE, TRUE, 0);
817   g_signal_connect(G_OBJECT(g->Dmin_picker), "color-set", G_CALLBACK(Dmin_picker_callback), self);
818 
819   g->Dmin_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row1);
820   gtk_widget_set_tooltip_text(g->Dmin_sampler , _("pick color of film material from image"));
821 
822   gtk_box_pack_start(GTK_BOX(page1), GTK_WIDGET(row1), FALSE, FALSE, 0);
823 
824   g->Dmin_R = dt_bauhaus_slider_from_params(self, "Dmin[0]");
825   dt_bauhaus_slider_set_digits(g->Dmin_R, 4);
826   dt_bauhaus_slider_set_step(g->Dmin_R, 0.0025);
827   dt_bauhaus_slider_set_format(g->Dmin_R, "%.2f %%");
828   dt_bauhaus_slider_set_factor(g->Dmin_R, 100);
829   dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min red component"));
830   gtk_widget_set_tooltip_text(g->Dmin_R, _("adjust the color and shade of the film transparent base.\n"
831                                            "this value depends on the film material, \n"
832                                            "the chemical fog produced while developing the film,\n"
833                                            "and the scanner white balance."));
834 
835   g->Dmin_G = dt_bauhaus_slider_from_params(self, "Dmin[1]");
836   dt_bauhaus_slider_set_digits(g->Dmin_G, 4);
837   dt_bauhaus_slider_set_step(g->Dmin_G, 0.0025);
838   dt_bauhaus_slider_set_format(g->Dmin_G, "%.2f %%");
839   dt_bauhaus_slider_set_factor(g->Dmin_G, 100);
840   dt_bauhaus_widget_set_label(g->Dmin_G, NULL, N_("D min green component"));
841   gtk_widget_set_tooltip_text(g->Dmin_G, _("adjust the color and shade of the film transparent base.\n"
842                                            "this value depends on the film material, \n"
843                                            "the chemical fog produced while developing the film,\n"
844                                            "and the scanner white balance."));
845 
846   g->Dmin_B = dt_bauhaus_slider_from_params(self, "Dmin[2]");
847   dt_bauhaus_slider_set_digits(g->Dmin_B, 4);
848   dt_bauhaus_slider_set_step(g->Dmin_B, 0.0025);
849   dt_bauhaus_slider_set_format(g->Dmin_B, "%.2f %%");
850   dt_bauhaus_slider_set_factor(g->Dmin_B, 100);
851   dt_bauhaus_widget_set_label(g->Dmin_B, NULL, N_("D min blue component"));
852   gtk_widget_set_tooltip_text(g->Dmin_B, _("adjust the color and shade of the film transparent base.\n"
853                                            "this value depends on the film material, \n"
854                                            "the chemical fog produced while developing the film,\n"
855                                            "and the scanner white balance."));
856 
857   // D max and scanner bias
858 
859   gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(_("dynamic range of the film")), FALSE, FALSE, 0);
860 
861   g->D_max = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "D_max"));
862   dt_bauhaus_slider_set_format(g->D_max, "%.2f dB");
863   gtk_widget_set_tooltip_text(g->D_max, _("maximum density of the film, corresponding to white after inversion.\n"
864                                           "this value depends on the film specifications, the developing process,\n"
865                                           "the dynamic range of the scene and the scanner exposure settings."));
866 
867   gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(_("scanner exposure settings")), FALSE, FALSE, 0);
868 
869   g->offset = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "offset"));
870   dt_bauhaus_slider_set_format(g->offset, "%+.2f dB");
871   dt_color_picker_new(self, DT_COLOR_PICKER_AREA, g->offset);
872   gtk_widget_set_tooltip_text(g->offset, _("correct the exposure of the scanner, for all RGB channels,\n"
873                                            "before the inversion, so blacks are neither clipped or too pale."));
874 
875   // Page CORRECTIONS
876   GtkWidget *page2 = self->widget = dt_ui_notebook_page(g->notebook, N_("corrections"), NULL);
877 
878   // WB shadows
879   gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(_("shadows color cast")), FALSE, FALSE, 0);
880 
881   GtkWidget *row3 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
882 
883   g->WB_low_picker = gtk_color_button_new();
884   gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->WB_low_picker), FALSE);
885   gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_low_picker), _("select color of shadows from a swatch"));
886   gtk_box_pack_start(GTK_BOX(row3), GTK_WIDGET(g->WB_low_picker), TRUE, TRUE, 0);
887   g_signal_connect(G_OBJECT(g->WB_low_picker), "color-set", G_CALLBACK(WB_low_picker_callback), self);
888 
889   g->WB_low_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row3);
890   gtk_widget_set_tooltip_text(g->WB_low_sampler, _("pick shadows color from image"));
891 
892   gtk_box_pack_start(GTK_BOX(page2), GTK_WIDGET(row3), FALSE, FALSE, 0);
893 
894   g->wb_low_R = dt_bauhaus_slider_from_params(self, "wb_low[0]");
895   dt_bauhaus_widget_set_label(g->wb_low_R, NULL, N_("shadows red offset"));
896   gtk_widget_set_tooltip_text(g->wb_low_R, _("correct the color cast in shadows so blacks are\n"
897                                              "truly achromatic. Setting this value before\n"
898                                              "the highlights illuminant white balance will help\n"
899                                              "recovering the global white balance in difficult cases."));
900 
901   g->wb_low_G = dt_bauhaus_slider_from_params(self, "wb_low[1]");
902   dt_bauhaus_widget_set_label(g->wb_low_G, NULL, N_("shadows green offset"));
903   gtk_widget_set_tooltip_text(g->wb_low_G, _("correct the color cast in shadows so blacks are\n"
904                                              "truly achromatic. Setting this value before\n"
905                                              "the highlights illuminant white balance will help\n"
906                                              "recovering the global white balance in difficult cases."));
907 
908   g->wb_low_B = dt_bauhaus_slider_from_params(self, "wb_low[2]");
909   dt_bauhaus_widget_set_label(g->wb_low_B, NULL, N_("shadows blue offset"));
910   gtk_widget_set_tooltip_text(g->wb_low_B, _("correct the color cast in shadows so blacks are\n"
911                                              "truly achromatic. Setting this value before\n"
912                                              "the highlights illuminant white balance will help\n"
913                                              "recovering the global white balance in difficult cases."));
914 
915   // WB highlights
916   gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(_("highlights white balance")), FALSE, FALSE, 0);
917 
918   GtkWidget *row2 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
919 
920   g->WB_high_picker = gtk_color_button_new();
921   gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->WB_high_picker), FALSE);
922   gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_high_picker), _("select color of illuminant from a swatch"));
923   gtk_box_pack_start(GTK_BOX(row2), GTK_WIDGET(g->WB_high_picker), TRUE, TRUE, 0);
924   g_signal_connect(G_OBJECT(g->WB_high_picker), "color-set", G_CALLBACK(WB_high_picker_callback), self);
925 
926   g->WB_high_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row2);
927   gtk_widget_set_tooltip_text(g->WB_high_sampler , _("pick illuminant color from image"));
928 
929   gtk_box_pack_start(GTK_BOX(page2), GTK_WIDGET(row2), FALSE, FALSE, 0);
930 
931   g->wb_high_R = dt_bauhaus_slider_from_params(self, "wb_high[0]");
932   dt_bauhaus_widget_set_label(g->wb_high_R, NULL, N_("illuminant red gain"));
933   gtk_widget_set_tooltip_text(g->wb_high_R, _("correct the color of the illuminant so whites are\n"
934                                               "truly achromatic. Setting this value after\n"
935                                               "the shadows color cast will help\n"
936                                               "recovering the global white balance in difficult cases."));
937 
938   g->wb_high_G = dt_bauhaus_slider_from_params(self, "wb_high[1]");
939   dt_bauhaus_widget_set_label(g->wb_high_G, NULL, N_("illuminant green gain"));
940   gtk_widget_set_tooltip_text(g->wb_high_G, _("correct the color of the illuminant so whites are\n"
941                                               "truly achromatic. Setting this value after\n"
942                                               "the shadows color cast will help\n"
943                                               "recovering the global white balance in difficult cases."));
944 
945   g->wb_high_B = dt_bauhaus_slider_from_params(self, "wb_high[2]");
946   dt_bauhaus_widget_set_label(g->wb_high_B, NULL, N_("illuminant blue gain"));
947   gtk_widget_set_tooltip_text(g->wb_high_B, _("correct the color of the illuminant so whites are\n"
948                                               "truly achromatic. Setting this value after\n"
949                                               "the shadows color cast will help\n"
950                                               "recovering the global white balance in difficult cases."));
951 
952   // Page PRINT PROPERTIES
953   GtkWidget *page3 = self->widget = dt_ui_notebook_page(g->notebook, N_("print properties"), NULL);
954 
955   // print corrections
956   gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(_("virtual paper properties")), FALSE, FALSE, 0);
957 
958   g->black = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "black"));
959   dt_bauhaus_slider_set_digits(g->black, 4);
960   dt_bauhaus_slider_set_step(g->black, 0.0005);
961   dt_bauhaus_slider_set_factor(g->black, 100);
962   dt_bauhaus_slider_set_format(g->black, "%+.2f %%");
963   gtk_widget_set_tooltip_text(g->black, _("correct the density of black after the inversion,\n"
964                                           "to adjust the global contrast while avoiding clipping shadows."));
965 
966   g->gamma = dt_bauhaus_slider_from_params(self, "gamma");
967   dt_bauhaus_widget_set_label(g->gamma, NULL, N_("paper grade (gamma)"));
968   gtk_widget_set_tooltip_text(g->gamma, _("select the grade of the virtual paper, which is actually\n"
969                                           "equivalent to applying a gamma. it compensates the film D max\n"
970                                           "and recovers the contrast. use a high grade for high D max."));
971 
972   g->soft_clip = dt_bauhaus_slider_from_params(self, "soft_clip");
973   dt_bauhaus_slider_set_factor(g->soft_clip, 100);
974   dt_bauhaus_slider_set_digits(g->soft_clip, 4);
975   dt_bauhaus_slider_set_format(g->soft_clip, "%.2f %%");
976   gtk_widget_set_tooltip_text(g->soft_clip, _("gradually compress specular highlights past this value\n"
977                                               "to avoid clipping while pushing the exposure for mid-tones.\n"
978                                               "this somewhat reproduces the behaviour of matte paper."));
979 
980   gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(_("virtual print emulation")), FALSE, FALSE, 0);
981 
982   g->exposure = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "exposure"));
983   dt_bauhaus_slider_set_hard_min(g->exposure, -1.0);
984   dt_bauhaus_slider_set_soft_min(g->exposure, -1.0);
985   dt_bauhaus_slider_set_hard_max(g->exposure, 1.0);
986   dt_bauhaus_slider_set_default(g->exposure, 0.0);
987   dt_bauhaus_slider_set_format(g->exposure, "%+.2f EV");
988   gtk_widget_set_tooltip_text(g->exposure, _("correct the printing exposure after inversion to adjust\n"
989                                              "the global contrast and avoid clipping highlights."));
990 
991   // start building top level widget
992   self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE);
993 
994   // Film emulsion
995   g->film_stock = dt_bauhaus_combobox_from_params(self, "film_stock");
996   gtk_widget_set_tooltip_text(g->film_stock, _("toggle on or off the color controls"));
997 
998   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
999 }
1000 
1001 
gui_changed(dt_iop_module_t * self,GtkWidget * w,void * previous)1002 void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
1003 {
1004   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
1005   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
1006   if(!w || w == g->film_stock)
1007   {
1008     toggle_stock_controls(self);
1009     Dmin_picker_update(self);
1010   }
1011   else if(w == g->Dmin_R && p->film_stock == DT_FILMSTOCK_NB)
1012   {
1013     dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[0]);
1014     dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[0]);
1015   }
1016   else if(w == g->Dmin_R || w == g->Dmin_G || w == g->Dmin_B)
1017   {
1018     Dmin_picker_update(self);
1019   }
1020   else if(w == g->exposure)
1021   {
1022     p->exposure = powf(2.0f, p->exposure);
1023   }
1024 
1025   if(!w || w == g->wb_high_R || w == g->wb_high_G || w == g->wb_high_B)
1026   {
1027     WB_high_picker_update(self);
1028   }
1029 
1030   if(!w || w == g->wb_low_R || w == g->wb_low_G || w == g->wb_low_B)
1031   {
1032     WB_low_picker_update(self);
1033   }
1034 }
1035 
1036 
gui_update(dt_iop_module_t * const self)1037 void gui_update(dt_iop_module_t *const self)
1038 {
1039   // let gui slider match current parameters:
1040   dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
1041   const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)self->params;
1042 
1043   dt_iop_color_picker_reset(self, TRUE);
1044 
1045   dt_bauhaus_combobox_set(g->film_stock, p->film_stock);
1046 
1047   // Dmin
1048   dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]);
1049   dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]);
1050   dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]);
1051 
1052   // Dmax
1053   dt_bauhaus_slider_set(g->D_max, p->D_max);
1054 
1055   // Scanner exposure offset
1056   dt_bauhaus_slider_set(g->offset, p->offset);
1057 
1058   // WB_high
1059   dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]);
1060   dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]);
1061   dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]);
1062 
1063   // WB_low
1064   dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]);
1065   dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]);
1066   dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]);
1067 
1068   // Print
1069   dt_bauhaus_slider_set(g->exposure, log2f(p->exposure));     // warning: GUI is in EV
1070   dt_bauhaus_slider_set(g->black, p->black);
1071   dt_bauhaus_slider_set(g->gamma, p->gamma);
1072   dt_bauhaus_slider_set(g->soft_clip, p->soft_clip);
1073 
1074   // Update custom stuff
1075   gui_changed(self, NULL, NULL);
1076 }
1077 
gui_reset(dt_iop_module_t * self)1078 void gui_reset(dt_iop_module_t *self)
1079 {
1080   dt_iop_color_picker_reset(self, TRUE);
1081 }
1082