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(¬ebook_def);
802 dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_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