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   float DT_ALIGNED_PIXEL Dmin[4];         // color of film substrate
111   float DT_ALIGNED_PIXEL wb_high[4];      // white balance RGB coeffs / Dmax
112   float DT_ALIGNED_PIXEL offset[4];       // 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       float DT_ALIGNED_PIXEL Dmin[4];         // color of film substrate
188       float DT_ALIGNED_PIXEL wb_high[4];      // white balance RGB coeffs (illuminant)
189       float DT_ALIGNED_PIXEL wb_low[4];       // 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   float WB_low_invert[3];
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   float RGB[3];
537   RGB[0] = 2.0f - c.red;
538   RGB[1] = 2.0f - c.green;
539   RGB[2] = 2.0f - c.blue;
540 
541   float RGB_min = v_minf(RGB);
542   for(size_t k = 0; k < 3; k++) p->wb_low[k] = RGB[k] / RGB_min;
543 
544   ++darktable.gui->reset;
545   dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]);
546   dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]);
547   dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]);
548   --darktable.gui->reset;
549 
550   WB_low_picker_update(self);
551   dt_iop_color_picker_reset(self, TRUE);
552   dt_dev_add_history_item(darktable.develop, self, TRUE);
553 }
554 
555 
WB_high_picker_update(dt_iop_module_t * self)556 static void WB_high_picker_update(dt_iop_module_t *self)
557 {
558   dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
559   const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)self->params;
560 
561   GdkRGBA color;
562   color.alpha = 1.0f;
563 
564   float WB_high_invert[3];
565   for(size_t c = 0; c < 3; ++c) WB_high_invert[c] = 2.0f - p->wb_high[c];
566   const float WB_high_max = v_maxf(WB_high_invert);
567   for(size_t c = 0; c < 3; ++c) WB_high_invert[c] /= WB_high_max;
568 
569   color.red = WB_high_invert[0];
570   color.green = WB_high_invert[1];
571   color.blue = WB_high_invert[2];
572 
573   gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->WB_high_picker), &color);
574 }
575 
WB_high_picker_callback(GtkColorButton * widget,dt_iop_module_t * self)576 static void WB_high_picker_callback(GtkColorButton *widget, dt_iop_module_t *self)
577 {
578   if(darktable.gui->reset) return;
579   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
580   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
581 
582   dt_iop_color_picker_reset(self, TRUE);
583 
584   GdkRGBA c;
585   gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c);
586 
587   float RGB[3];
588   RGB[0] = 2.0f - c.red;
589   RGB[1] = 2.0f - c.green;
590   RGB[2] = 2.0f - c.blue;
591 
592   float RGB_min = v_minf(RGB);
593   for(size_t k = 0; k < 3; k++) p->wb_high[k] = RGB[k] / RGB_min;
594 
595   ++darktable.gui->reset;
596   dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]);
597   dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]);
598   dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]);
599   --darktable.gui->reset;
600 
601   WB_high_picker_update(self);
602   dt_iop_color_picker_reset(self, TRUE);
603   dt_dev_add_history_item(darktable.develop, self, TRUE);
604 }
605 
606 
607 /* Color pickers auto-tuners */
608 
609 // measure Dmin from the film edges first
apply_auto_Dmin(dt_iop_module_t * self)610 static void apply_auto_Dmin(dt_iop_module_t *self)
611 {
612   if(darktable.gui->reset) return;
613   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
614   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
615 
616   for(int k = 0; k < 4; k++) p->Dmin[k] = self->picked_color[k];
617 
618   ++darktable.gui->reset;
619   dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]);
620   dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]);
621   dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]);
622   --darktable.gui->reset;
623 
624   Dmin_picker_update(self);
625   dt_control_queue_redraw_widget(self->widget);
626   dt_dev_add_history_item(darktable.develop, self, TRUE);
627 }
628 
629 // from Dmin, find out the range of density values of the film and compute Dmax
apply_auto_Dmax(dt_iop_module_t * self)630 static void apply_auto_Dmax(dt_iop_module_t *self)
631 {
632   if(darktable.gui->reset) return;
633   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
634   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
635 
636   float RGB[3];
637   for(int c = 0; c < 3; c++)
638   {
639     RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD));
640   }
641 
642   // Take the max(RGB) for safety. Big values unclip whites
643   p->D_max = v_maxf(RGB);
644 
645   ++darktable.gui->reset;
646   dt_bauhaus_slider_set(g->D_max, p->D_max);
647   --darktable.gui->reset;
648 
649   dt_control_queue_redraw_widget(self->widget);
650   dt_dev_add_history_item(darktable.develop, self, TRUE);
651 }
652 
653 // from Dmax, compute the offset so the range of density is rescaled between [0; 1]
apply_auto_offset(dt_iop_module_t * self)654 static void apply_auto_offset(dt_iop_module_t *self)
655 {
656   if(darktable.gui->reset) return;
657   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
658   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
659 
660   float RGB[3];
661   for(int c = 0; c < 3; c++)
662     RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)) / p->D_max;
663 
664   // Take the min(RGB) for safety. Negative values unclip blacks
665   p->offset = v_minf(RGB);
666 
667   ++darktable.gui->reset;
668   dt_bauhaus_slider_set(g->offset, p->offset);
669   --darktable.gui->reset;
670 
671   dt_control_queue_redraw_widget(self->widget);
672   dt_dev_add_history_item(darktable.develop, self, TRUE);
673 }
674 
675 // from Dmax and offset, compute the white balance correction as multipliers of the offset
676 // such that offset × wb[c] make black monochrome
apply_auto_WB_low(dt_iop_module_t * self)677 static void apply_auto_WB_low(dt_iop_module_t *self)
678 {
679   if(darktable.gui->reset) return;
680   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
681   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
682 
683   float RGB_min[3];
684   for(int c = 0; c < 3; c++)
685     RGB_min[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)) / p->D_max;
686 
687   const float RGB_v_min = v_minf(RGB_min); // warning: can be negative
688   for(int c = 0; c < 3; c++) p->wb_low[c] =  RGB_v_min / RGB_min[c];
689 
690   ++darktable.gui->reset;
691   dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]);
692   dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]);
693   dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]);
694   --darktable.gui->reset;
695 
696   WB_low_picker_update(self);
697   dt_control_queue_redraw_widget(self->widget);
698   dt_dev_add_history_item(darktable.develop, self, TRUE);
699 }
700 
701 // from Dmax, offset and white balance multipliers, compute the white balance of the illuminant as multipliers of 1/Dmax
702 // such that WB[c] / Dmax make white monochrome
apply_auto_WB_high(dt_iop_module_t * self)703 static void apply_auto_WB_high(dt_iop_module_t *self)
704 {
705   if(darktable.gui->reset) return;
706   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
707   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
708 
709   float RGB_min[3];
710   for(int c = 0; c < 3; c++)
711     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));
712 
713   const float RGB_v_min = v_minf(RGB_min); // warning : must be positive
714   for(int c = 0; c < 3; c++) p->wb_high[c] = RGB_min[c] / RGB_v_min;
715 
716   ++darktable.gui->reset;
717   dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]);
718   dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]);
719   dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]);
720   --darktable.gui->reset;
721 
722   WB_high_picker_update(self);
723   dt_control_queue_redraw_widget(self->widget);
724   dt_dev_add_history_item(darktable.develop, self, TRUE);
725 }
726 
727 // from Dmax, offset and both white balances, compute the print black adjustment
728 // such that the printed values range from 0 to + infinity
apply_auto_black(dt_iop_module_t * self)729 static void apply_auto_black(dt_iop_module_t *self)
730 {
731   if(darktable.gui->reset) return;
732   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
733   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
734 
735   float RGB[3];
736   for(int c = 0; c < 3; c++)
737   {
738     RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD));
739     RGB[c] *= p->wb_high[c] / p->D_max;
740     RGB[c] += p->wb_low[c] * p->offset * p->wb_high[c];
741     RGB[c] = 0.1f - (1.0f - fast_exp10f(RGB[c])); // actually, remap between -3.32 EV and infinity for safety because gamma comes later
742   }
743   p->black = v_maxf(RGB);
744 
745   ++darktable.gui->reset;
746   dt_bauhaus_slider_set(g->black, p->black);
747   --darktable.gui->reset;
748 
749   dt_control_queue_redraw_widget(self->widget);
750   dt_dev_add_history_item(darktable.develop, self, TRUE);
751 }
752 
753 // from Dmax, offset, both white balances, and printblack, compute the print exposure adjustment as a scaling factor
754 // such that the printed values range from 0 to 1
apply_auto_exposure(dt_iop_module_t * self)755 static void apply_auto_exposure(dt_iop_module_t *self)
756 {
757   if(darktable.gui->reset) return;
758   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
759   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
760 
761   float RGB[3];
762   for(int c = 0; c < 3; c++)
763   {
764     RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD));
765     RGB[c] *= p->wb_high[c] / p->D_max;
766     RGB[c] += p->wb_low[c] * p->offset;
767     RGB[c] = 0.96f / (1.0f - fast_exp10f(RGB[c]) + p->black); // actually, remap in [0; 0.96] for safety
768   }
769   p->exposure = v_minf(RGB);
770 
771   ++darktable.gui->reset;
772   dt_bauhaus_slider_set(g->exposure, log2f(p->exposure));
773   --darktable.gui->reset;
774 
775   dt_control_queue_redraw_widget(self->widget);
776   dt_dev_add_history_item(darktable.develop, self, TRUE);
777 }
778 
779 
color_picker_apply(dt_iop_module_t * self,GtkWidget * picker,dt_dev_pixelpipe_iop_t * piece)780 void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_iop_t *piece)
781 {
782   if(darktable.gui->reset) return;
783   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
784 
785   if     (picker == g->Dmin_sampler)
786     apply_auto_Dmin(self);
787   else if(picker == g->WB_high_sampler)
788     apply_auto_WB_high(self);
789   else if(picker == g->offset)
790     apply_auto_offset(self);
791   else if(picker == g->D_max)
792     apply_auto_Dmax(self);
793   else if(picker == g->WB_low_sampler)
794     apply_auto_WB_low(self);
795   else if(picker == g->exposure)
796     apply_auto_exposure(self);
797   else if(picker == g->black)
798     apply_auto_black(self);
799   else
800     fprintf(stderr, "[negadoctor] unknown color picker\n");
801 }
802 
gui_init(dt_iop_module_t * self)803 void gui_init(dt_iop_module_t *self)
804 {
805   dt_iop_negadoctor_gui_data_t *g = IOP_GUI_ALLOC(negadoctor);
806 
807   g->notebook = GTK_NOTEBOOK(gtk_notebook_new());
808 
809   // Page FILM PROPERTIES
810   GtkWidget *page1 = self->widget = dt_ui_notebook_page(g->notebook, _("film properties"), NULL);
811 
812   // Dmin
813 
814   gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(_("color of the film base")), FALSE, FALSE, 0);
815 
816   GtkWidget *row1 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
817 
818   g->Dmin_picker = gtk_color_button_new();
819   gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->Dmin_picker), FALSE);
820   gtk_color_button_set_title(GTK_COLOR_BUTTON(g->Dmin_picker), _("select color of film material from a swatch"));
821   gtk_box_pack_start(GTK_BOX(row1), GTK_WIDGET(g->Dmin_picker), TRUE, TRUE, 0);
822   g_signal_connect(G_OBJECT(g->Dmin_picker), "color-set", G_CALLBACK(Dmin_picker_callback), self);
823 
824   g->Dmin_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row1);
825   gtk_widget_set_tooltip_text(g->Dmin_sampler , _("pick color of film material from image"));
826 
827   gtk_box_pack_start(GTK_BOX(page1), GTK_WIDGET(row1), FALSE, FALSE, 0);
828 
829   g->Dmin_R = dt_bauhaus_slider_from_params(self, "Dmin[0]");
830   dt_bauhaus_slider_set_digits(g->Dmin_R, 4);
831   dt_bauhaus_slider_set_step(g->Dmin_R, 0.0025);
832   dt_bauhaus_slider_set_format(g->Dmin_R, "%.2f %%");
833   dt_bauhaus_slider_set_factor(g->Dmin_R, 100);
834   dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min red component"));
835   gtk_widget_set_tooltip_text(g->Dmin_R, _("adjust the color and shade of the film transparent base.\n"
836                                            "this value depends on the film material, \n"
837                                            "the chemical fog produced while developing the film,\n"
838                                            "and the scanner white balance."));
839 
840   g->Dmin_G = dt_bauhaus_slider_from_params(self, "Dmin[1]");
841   dt_bauhaus_slider_set_digits(g->Dmin_G, 4);
842   dt_bauhaus_slider_set_step(g->Dmin_G, 0.0025);
843   dt_bauhaus_slider_set_format(g->Dmin_G, "%.2f %%");
844   dt_bauhaus_slider_set_factor(g->Dmin_G, 100);
845   dt_bauhaus_widget_set_label(g->Dmin_G, NULL, N_("D min green component"));
846   gtk_widget_set_tooltip_text(g->Dmin_G, _("adjust the color and shade of the film transparent base.\n"
847                                            "this value depends on the film material, \n"
848                                            "the chemical fog produced while developing the film,\n"
849                                            "and the scanner white balance."));
850 
851   g->Dmin_B = dt_bauhaus_slider_from_params(self, "Dmin[2]");
852   dt_bauhaus_slider_set_digits(g->Dmin_B, 4);
853   dt_bauhaus_slider_set_step(g->Dmin_B, 0.0025);
854   dt_bauhaus_slider_set_format(g->Dmin_B, "%.2f %%");
855   dt_bauhaus_slider_set_factor(g->Dmin_B, 100);
856   dt_bauhaus_widget_set_label(g->Dmin_B, NULL, N_("D min blue component"));
857   gtk_widget_set_tooltip_text(g->Dmin_B, _("adjust the color and shade of the film transparent base.\n"
858                                            "this value depends on the film material, \n"
859                                            "the chemical fog produced while developing the film,\n"
860                                            "and the scanner white balance."));
861 
862   // D max and scanner bias
863 
864   gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(_("dynamic range of the film")), FALSE, FALSE, 0);
865 
866   g->D_max = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "D_max"));
867   dt_bauhaus_slider_set_format(g->D_max, "%.2f dB");
868   gtk_widget_set_tooltip_text(g->D_max, _("maximum density of the film, corresponding to white after inversion.\n"
869                                           "this value depends on the film specifications, the developing process,\n"
870                                           "the dynamic range of the scene and the scanner exposure settings."));
871 
872   gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(_("scanner exposure settings")), FALSE, FALSE, 0);
873 
874   g->offset = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "offset"));
875   dt_bauhaus_slider_set_format(g->offset, "%+.2f dB");
876   dt_color_picker_new(self, DT_COLOR_PICKER_AREA, g->offset);
877   gtk_widget_set_tooltip_text(g->offset, _("correct the exposure of the scanner, for all RGB channels,\n"
878                                            "before the inversion, so blacks are neither clipped or too pale."));
879 
880   // Page CORRECTIONS
881   GtkWidget *page2 = self->widget = dt_ui_notebook_page(g->notebook, _("corrections"), NULL);
882 
883   // WB shadows
884   gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(_("shadows color cast")), FALSE, FALSE, 0);
885 
886   GtkWidget *row3 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
887 
888   g->WB_low_picker = gtk_color_button_new();
889   gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->WB_low_picker), FALSE);
890   gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_low_picker), _("select color of shadows from a swatch"));
891   gtk_box_pack_start(GTK_BOX(row3), GTK_WIDGET(g->WB_low_picker), TRUE, TRUE, 0);
892   g_signal_connect(G_OBJECT(g->WB_low_picker), "color-set", G_CALLBACK(WB_low_picker_callback), self);
893 
894   g->WB_low_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row3);
895   gtk_widget_set_tooltip_text(g->WB_low_sampler, _("pick shadows color from image"));
896 
897   gtk_box_pack_start(GTK_BOX(page2), GTK_WIDGET(row3), FALSE, FALSE, 0);
898 
899   g->wb_low_R = dt_bauhaus_slider_from_params(self, "wb_low[0]");
900   dt_bauhaus_widget_set_label(g->wb_low_R, NULL, N_("shadows red offset"));
901   gtk_widget_set_tooltip_text(g->wb_low_R, _("correct the color cast in shadows so blacks are\n"
902                                              "truly achromatic. Setting this value before\n"
903                                              "the highlights illuminant white balance will help\n"
904                                              "recovering the global white balance in difficult cases."));
905 
906   g->wb_low_G = dt_bauhaus_slider_from_params(self, "wb_low[1]");
907   dt_bauhaus_widget_set_label(g->wb_low_G, NULL, N_("shadows green offset"));
908   gtk_widget_set_tooltip_text(g->wb_low_G, _("correct the color cast in shadows so blacks are\n"
909                                              "truly achromatic. Setting this value before\n"
910                                              "the highlights illuminant white balance will help\n"
911                                              "recovering the global white balance in difficult cases."));
912 
913   g->wb_low_B = dt_bauhaus_slider_from_params(self, "wb_low[2]");
914   dt_bauhaus_widget_set_label(g->wb_low_B, NULL, N_("shadows blue offset"));
915   gtk_widget_set_tooltip_text(g->wb_low_B, _("correct the color cast in shadows so blacks are\n"
916                                              "truly achromatic. Setting this value before\n"
917                                              "the highlights illuminant white balance will help\n"
918                                              "recovering the global white balance in difficult cases."));
919 
920   // WB highlights
921   gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(_("highlights white balance")), FALSE, FALSE, 0);
922 
923   GtkWidget *row2 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
924 
925   g->WB_high_picker = gtk_color_button_new();
926   gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->WB_high_picker), FALSE);
927   gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_high_picker), _("select color of illuminant from a swatch"));
928   gtk_box_pack_start(GTK_BOX(row2), GTK_WIDGET(g->WB_high_picker), TRUE, TRUE, 0);
929   g_signal_connect(G_OBJECT(g->WB_high_picker), "color-set", G_CALLBACK(WB_high_picker_callback), self);
930 
931   g->WB_high_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row2);
932   gtk_widget_set_tooltip_text(g->WB_high_sampler , _("pick illuminant color from image"));
933 
934   gtk_box_pack_start(GTK_BOX(page2), GTK_WIDGET(row2), FALSE, FALSE, 0);
935 
936   g->wb_high_R = dt_bauhaus_slider_from_params(self, "wb_high[0]");
937   dt_bauhaus_widget_set_label(g->wb_high_R, NULL, N_("illuminant red gain"));
938   gtk_widget_set_tooltip_text(g->wb_high_R, _("correct the color of the illuminant so whites are\n"
939                                               "truly achromatic. Setting this value after\n"
940                                               "the shadows color cast will help\n"
941                                               "recovering the global white balance in difficult cases."));
942 
943   g->wb_high_G = dt_bauhaus_slider_from_params(self, "wb_high[1]");
944   dt_bauhaus_widget_set_label(g->wb_high_G, NULL, N_("illuminant green gain"));
945   gtk_widget_set_tooltip_text(g->wb_high_G, _("correct the color of the illuminant so whites are\n"
946                                               "truly achromatic. Setting this value after\n"
947                                               "the shadows color cast will help\n"
948                                               "recovering the global white balance in difficult cases."));
949 
950   g->wb_high_B = dt_bauhaus_slider_from_params(self, "wb_high[2]");
951   dt_bauhaus_widget_set_label(g->wb_high_B, NULL, N_("illuminant blue gain"));
952   gtk_widget_set_tooltip_text(g->wb_high_B, _("correct the color of the illuminant so whites are\n"
953                                               "truly achromatic. Setting this value after\n"
954                                               "the shadows color cast will help\n"
955                                               "recovering the global white balance in difficult cases."));
956 
957   // Page PRINT PROPERTIES
958   GtkWidget *page3 = self->widget = dt_ui_notebook_page(g->notebook, _("print properties"), NULL);
959 
960   // print corrections
961   gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(_("virtual paper properties")), FALSE, FALSE, 0);
962 
963   g->black = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "black"));
964   dt_bauhaus_slider_set_digits(g->black, 4);
965   dt_bauhaus_slider_set_step(g->black, 0.0005);
966   dt_bauhaus_slider_set_factor(g->black, 100);
967   dt_bauhaus_slider_set_format(g->black, "%+.2f %%");
968   gtk_widget_set_tooltip_text(g->black, _("correct the density of black after the inversion,\n"
969                                           "to adjust the global contrast while avoiding clipping shadows."));
970 
971   g->gamma = dt_bauhaus_slider_from_params(self, "gamma");
972   dt_bauhaus_widget_set_label(g->gamma, NULL, N_("paper grade (gamma)"));
973   gtk_widget_set_tooltip_text(g->gamma, _("select the grade of the virtual paper, which is actually\n"
974                                           "equivalent to applying a gamma. it compensates the film D max\n"
975                                           "and recovers the contrast. use a high grade for high D max."));
976 
977   g->soft_clip = dt_bauhaus_slider_from_params(self, "soft_clip");
978   dt_bauhaus_slider_set_factor(g->soft_clip, 100);
979   dt_bauhaus_slider_set_digits(g->soft_clip, 4);
980   dt_bauhaus_slider_set_format(g->soft_clip, "%.2f %%");
981   gtk_widget_set_tooltip_text(g->soft_clip, _("gradually compress specular highlights past this value\n"
982                                               "to avoid clipping while pushing the exposure for midtones.\n"
983                                               "this somewhat reproduces the behaviour of matte paper."));
984 
985   gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(_("virtual print emulation")), FALSE, FALSE, 0);
986 
987   g->exposure = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "exposure"));
988   dt_bauhaus_slider_set_hard_min(g->exposure, -1.0);
989   dt_bauhaus_slider_set_soft_min(g->exposure, -1.0);
990   dt_bauhaus_slider_set_hard_max(g->exposure, 1.0);
991   dt_bauhaus_slider_set_default(g->exposure, 0.0);
992   dt_bauhaus_slider_set_format(g->exposure, "%+.2f EV");
993   gtk_widget_set_tooltip_text(g->exposure, _("correct the printing exposure after inversion to adjust\n"
994                                              "the global contrast and avoid clipping highlights."));
995 
996   // start building top level widget
997   self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE);
998 
999   // Film emulsion
1000   g->film_stock = dt_bauhaus_combobox_from_params(self, "film_stock");
1001   gtk_widget_set_tooltip_text(g->film_stock, _("toggle on or off the color controls"));
1002 
1003   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
1004 }
1005 
1006 
gui_changed(dt_iop_module_t * self,GtkWidget * w,void * previous)1007 void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
1008 {
1009   dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params;
1010   dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
1011   if(!w || w == g->film_stock)
1012   {
1013     toggle_stock_controls(self);
1014     Dmin_picker_update(self);
1015   }
1016   else if(w == g->Dmin_R && p->film_stock == DT_FILMSTOCK_NB)
1017   {
1018     dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[0]);
1019     dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[0]);
1020   }
1021   else if(w == g->Dmin_R || w == g->Dmin_G || w == g->Dmin_B)
1022   {
1023     Dmin_picker_update(self);
1024   }
1025   else if(w == g->exposure)
1026   {
1027     p->exposure = powf(2.0f, p->exposure);
1028   }
1029 
1030   if(!w || w == g->wb_high_R || w == g->wb_high_G || w == g->wb_high_B)
1031   {
1032     WB_high_picker_update(self);
1033   }
1034 
1035   if(!w || w == g->wb_low_R || w == g->wb_low_G || w == g->wb_low_B)
1036   {
1037     WB_low_picker_update(self);
1038   }
1039 }
1040 
1041 
gui_update(dt_iop_module_t * const self)1042 void gui_update(dt_iop_module_t *const self)
1043 {
1044   // let gui slider match current parameters:
1045   dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data;
1046   const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)self->params;
1047 
1048   dt_iop_color_picker_reset(self, TRUE);
1049 
1050   self->color_picker_box[0] = self->color_picker_box[1] = .10f;
1051   self->color_picker_box[2] = self->color_picker_box[3] = .50f;
1052   self->color_picker_point[0] = self->color_picker_point[1] = 0.5f;
1053 
1054   dt_bauhaus_combobox_set(g->film_stock, p->film_stock);
1055 
1056   // Dmin
1057   dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]);
1058   dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]);
1059   dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]);
1060 
1061   // Dmax
1062   dt_bauhaus_slider_set(g->D_max, p->D_max);
1063 
1064   // Scanner exposure offset
1065   dt_bauhaus_slider_set(g->offset, p->offset);
1066 
1067   // WB_high
1068   dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]);
1069   dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]);
1070   dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]);
1071 
1072   // WB_low
1073   dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]);
1074   dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]);
1075   dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]);
1076 
1077   // Print
1078   dt_bauhaus_slider_set(g->exposure, log2f(p->exposure));     // warning: GUI is in EV
1079   dt_bauhaus_slider_set(g->black, p->black);
1080   dt_bauhaus_slider_set(g->gamma, p->gamma);
1081   dt_bauhaus_slider_set(g->soft_clip, p->soft_clip);
1082 
1083   // Update custom stuff
1084   gui_changed(self, NULL, NULL);
1085 }
1086 
gui_reset(dt_iop_module_t * self)1087 void gui_reset(dt_iop_module_t *self)
1088 {
1089   dt_iop_color_picker_reset(self, TRUE);
1090 }
1091