Files
filmsim/docs/negadoctor.md
2025-06-05 21:06:50 -04:00

43 KiB
Raw Blame History

/* This file is part of darktable, Copyright (C) 2020-2024 darktable developers.

darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with darktable.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "bauhaus/bauhaus.h" #include "common/darktable.h" #include "common/opencl.h" #include "control/control.h" #include "develop/develop.h" #include "develop/imageop.h" #include "develop/imageop_math.h" #include "develop/imageop_gui.h" #include "develop/openmp_maths.h" #include "dtgtk/button.h" #include "dtgtk/resetlabel.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/presets.h" #include "gui/color_picker_proxy.h" #include "iop/iop_api.h"

#include <glib.h> #include <math.h> #include <stdlib.h>

/** DOCUMENTATION *

#define THRESHOLD 2.3283064365386963e-10f // -32 EV

DT_MODULE_INTROSPECTION(2, dt_iop_negadoctor_params_t)

typedef enum dt_iop_negadoctor_filmstock_t { // What kind of emulsion are we working on ? DT_FILMSTOCK_NB = 0, // $DESCRIPTION: "black and white film" DT_FILMSTOCK_COLOR = 1 // $DESCRIPTION: "color film" } dt_iop_negadoctor_filmstock_t;

typedef struct dt_iop_negadoctor_params_t { dt_iop_negadoctor_filmstock_t film_stock; /* $DEFAULT: DT_FILMSTOCK_COLOR $DESCRIPTION: "film stock" / float Dmin[4]; / color of film substrate $MIN: 0.00001 $MAX: 1.5 $DEFAULT: 1.0 / float wb_high[4]; / white balance RGB coeffs (illuminant) $MIN: 0.25 $MAX: 2 $DEFAULT: 1.0 / float wb_low[4]; / white balance RGB offsets (base light) $MIN: 0.25 $MAX: 2 $DEFAULT: 1.0 / float D_max; / max density of film $MIN: 0.1 $MAX: 6 $DEFAULT: 2.046 / float offset; / inversion offset $MIN: -1.0 $MAX: 1.0 $DEFAULT: -0.05 $DESCRIPTION: "scan exposure bias" / float black; / display black level $MIN: -0.5 $MAX: 0.5 $DEFAULT: 0.0755 $DESCRIPTION: "paper black (density correction)" / float gamma; / display gamma $MIN: 1.0 $MAX: 8.0 $DEFAULT: 4.0 $DESCRIPTION: "paper grade (gamma)" / float soft_clip; / highlights roll-off $MIN: 0.0001 $MAX: 1.0 $DEFAULT: 0.75 $DESCRIPTION: "paper gloss (specular highlights)" / float exposure; / extra exposure $MIN: 0.5 $MAX: 2.0 $DEFAULT: 0.9245 $DESCRIPTION: "print exposure adjustment" */ } dt_iop_negadoctor_params_t;

typedef struct dt_iop_negadoctor_data_t { dt_aligned_pixel_t Dmin; // color of film substrate dt_aligned_pixel_t wb_high; // white balance RGB coeffs / Dmax dt_aligned_pixel_t offset; // inversion offset float black; // display black level float gamma; // display gamma float soft_clip; // highlights roll-off float soft_clip_comp; // 1 - softclip, complement to 1 float exposure; // extra exposure } dt_iop_negadoctor_data_t;

typedef struct dt_iop_negadoctor_gui_data_t { GtkNotebook *notebook; GtkWidget *film_stock; GtkWidget *Dmin_R, *Dmin_G, *Dmin_B; GtkWidget *wb_high_R, *wb_high_G, *wb_high_B; GtkWidget *wb_low_R, *wb_low_G, *wb_low_B; GtkWidget *D_max; GtkWidget *offset; GtkWidget *black, *gamma, *soft_clip, *exposure; GtkWidget *Dmin_picker, *Dmin_sampler; GtkWidget *WB_high_picker, *WB_high_sampler; GtkWidget *WB_low_picker, *WB_low_sampler; } dt_iop_negadoctor_gui_data_t;

typedef struct dt_iop_negadoctor_global_data_t { int kernel_negadoctor; } dt_iop_negadoctor_global_data_t;

const char *name() { return _("negadoctor"); }

const char *aliases() { return _("film|invert|negative|scan"); }

const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("invert film negative scans and simulate printing on paper"), _("corrective and creative"), _("linear, RGB, display-referred"), _("non-linear, RGB"), _("non-linear, RGB, display-referred")); }

int flags() { return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_ONE_INSTANCE; }

int default_group() { return IOP_GROUP_BASIC | IOP_GROUP_TECHNICAL; }

dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return IOP_CS_RGB; }

int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void **new_params, int32_t *new_params_size, int *new_version) { typedef struct dt_iop_negadoctor_params_v2_t { dt_iop_negadoctor_filmstock_t film_stock; float Dmin[4]; float wb_high[4]; float wb_low[4]; float D_max; float offset; float black; float gamma; float soft_clip; float exposure; } dt_iop_negadoctor_params_v2_t;

if(old_version == 1) { typedef struct dt_iop_negadoctor_params_v1_t { dt_iop_negadoctor_filmstock_t film_stock; dt_aligned_pixel_t Dmin; // color of film substrate dt_aligned_pixel_t wb_high; // white balance RGB coeffs (illuminant) dt_aligned_pixel_t wb_low; // white balance RGB offsets (base light) float D_max; // max density of film float offset; // inversion offset float black; // display black level float gamma; // display gamma float soft_clip; // highlights roll-off float exposure; // extra exposure } dt_iop_negadoctor_params_v1_t;

const dt_iop_negadoctor_params_v1_t *o = (dt_iop_negadoctor_params_v1_t *)old_params;
dt_iop_negadoctor_params_v2_t *n = malloc(sizeof(dt_iop_negadoctor_params_v2_t));

// WARNING: when copying the arrays in a for loop, gcc wrongly assumed
//          that n and o were aligned and used AVX instructions for me,
//          which segfaulted. let's hope this doesn't get optimized too much.
n->film_stock = o->film_stock;
n->Dmin[0] = o->Dmin[0];
n->Dmin[1] = o->Dmin[1];
n->Dmin[2] = o->Dmin[2];
n->Dmin[3] = o->Dmin[3];
n->wb_high[0] = o->wb_high[0];
n->wb_high[1] = o->wb_high[1];
n->wb_high[2] = o->wb_high[2];
n->wb_high[3] = o->wb_high[3];
n->wb_low[0] = o->wb_low[0];
n->wb_low[1] = o->wb_low[1];
n->wb_low[2] = o->wb_low[2];
n->wb_low[3] = o->wb_low[3];
n->D_max = o->D_max;
n->offset = o->offset;
n->black = o->black;
n->gamma = o->gamma;
n->soft_clip = o->soft_clip;
n->exposure = o->exposure;

*new_params = n;
*new_params_size = sizeof(dt_iop_negadoctor_params_v2_t);
*new_version = 2;
return 0;

} return 1; }

void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)p1; dt_iop_negadoctor_data_t *const d = piece->data;

// keep WB_high even in B&W mode to apply sepia or warm tone look // but premultiply it aheard with Dmax to spare one div per pixel for(size_t c = 0; c < 4; c++) d->wb_high[c] = p->wb_high[c] / p->D_max;

for(size_t c = 0; c < 4; c++) d->offset[c] = p->wb_high[c] * p->offset * p->wb_low[c];

// ensure we use a monochrome Dmin for B&W film if(p->film_stock == DT_FILMSTOCK_COLOR) for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[c]; else if(p->film_stock == DT_FILMSTOCK_NB) for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[0];

// arithmetic trick allowing to rewrite the pixel inversion as FMA d->black = -p->exposure * (1.0f + p->black);

// highlights soft clip d->soft_clip = p->soft_clip; d->soft_clip_comp = 1.0f - p->soft_clip;

// copy d->exposure = p->exposure; d->gamma = p->gamma; }

static inline void _process_pixel(const dt_aligned_pixel_t pix_in, dt_aligned_pixel_t pix_out, const dt_aligned_pixel_t Dmin, const dt_aligned_pixel_t wb_high, const dt_aligned_pixel_t offset, const dt_aligned_pixel_t black, const dt_aligned_pixel_t exposure, const dt_aligned_pixel_t gamma, const dt_aligned_pixel_t soft_clip, const dt_aligned_pixel_t soft_clip_comp) { dt_aligned_pixel_t density; // Convert transmission to density using Dmin as a fulcrum dt_aligned_pixel_t clamped; for_each_channel(c) { clamped[c] = MAX(pix_in[c],THRESHOLD); // threshold to -32 EV density[c] = Dmin[c] / clamped[c]; } dt_aligned_pixel_t log_density; dt_vector_log2(density, log_density); #define LOG2_to_LOG10 0.3010299956f for_each_channel(c) log_density[c] *= -LOG2_to_LOG10; // now log_density = -log10f( Dmin / MAX(pix_in, THRESHOLD) ) dt_aligned_pixel_t corrected_de; for_each_channel(c) { // Correct density in log space corrected_de[c] = wb_high[c] * log_density[c] + offset[c]; } dt_aligned_pixel_t ten_to_x; dt_vector_exp10(corrected_de, ten_to_x); dt_aligned_pixel_t print_linear; for_each_channel(c) { // Print density on paper : ((1 - 10^corrected_de + black) * exposure)^gamma rewritten for FMA print_linear[c] = -(exposure[c] * ten_to_x[c] + black[c]); print_linear[c] = MAX(print_linear[c], 0.0f); } dt_aligned_pixel_t print_gamma; dt_vector_powf(print_linear, gamma, print_gamma); // note : this is always > 0 dt_aligned_pixel_t e_to_gamma; dt_aligned_pixel_t clipped_gamma; for_each_channel(c) clipped_gamma[c] = -(print_gamma[c] - soft_clip[c]) / soft_clip_comp[c]; dt_vector_exp(clipped_gamma, e_to_gamma); for_each_channel(c) { // Compress highlights. from https://lists.gnu.org/archive/html/openexr-devel/2005-03/msg00009.html pix_out[c] = (print_gamma[c] > soft_clip[c]) ? soft_clip[c] + (1.0f - e_to_gamma[c]) * soft_clip_comp[c] : print_gamma[c]; } }

void process(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) { const dt_iop_negadoctor_data_t *const d = piece->data; assert(piece->colors = 4);

const float *const restrict in = (float *)ivoid; float *const restrict out = (float *)ovoid;

dt_aligned_pixel_t gamma; dt_aligned_pixel_t black; dt_aligned_pixel_t exposure; dt_aligned_pixel_t soft_clip; dt_aligned_pixel_t soft_clip_comp; for_each_channel(c) { gamma[c] = d->gamma; black[c] = d->black; exposure[c] = d->exposure; soft_clip[c] = d->soft_clip; soft_clip_comp[c] = d->soft_clip_comp; } // Unpack vectors one by one with extra pragmas to be sure the compiler understands they can be vectorized const float *const restrict Dmin = DT_IS_ALIGNED_PIXEL(d->Dmin); const float *const restrict wb_high = DT_IS_ALIGNED_PIXEL(d->wb_high); const float *const restrict offset = DT_IS_ALIGNED_PIXEL(d->offset);

DT_OMP_FOR() for(size_t k = 0; k < (size_t)roi_out->height * roi_out->width * 4; k += 4) { const float *const restrict pix_in = in + k; float *const restrict pix_out = out + k; _process_pixel(pix_in, pix_out, Dmin, wb_high, offset, black, exposure, gamma, soft_clip, soft_clip_comp); } }

#ifdef HAVE_OPENCL int process_cl(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) { const dt_iop_negadoctor_data_t *const d = piece->data; const dt_iop_negadoctor_global_data_t *const gd = self->global_data;

const int devid = piece->pipe->devid; const int width = roi_in->width; const int height = roi_in->height;

return dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_negadoctor, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(d->Dmin), CLARG(d->wb_high), CLARG(d->offset), CLARG(d->exposure), CLARG(d->black), CLARG(d->gamma), CLARG(d->soft_clip), CLARG(d->soft_clip_comp)); } #endif

void init(dt_iop_module_t *self) { dt_iop_default_init(self);

dt_iop_negadoctor_params_t *d = self->default_params;

d->Dmin[0] = 1.00f; d->Dmin[1] = 0.45f; d->Dmin[2] = 0.25f; d->Dmin[3] = 1.00f; // keep parameter validation with -d common happy }

void init_presets(dt_iop_module_so_t *self) { dt_iop_negadoctor_params_t tmp = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_COLOR, .Dmin = { 1.13f, 0.49f, 0.27f, 0.0f}, .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f }, .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f }, .D_max = 1.6f, .offset = -0.05f, .gamma = 4.0f, .soft_clip = 0.75f, .exposure = 0.9245f, .black = 0.0755f };

dt_gui_presets_add_generic(_("color film"), self->op, self->version(), &tmp, sizeof(tmp), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);

dt_iop_negadoctor_params_t tmq = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_NB, .Dmin = { 1.0f, 1.0f, 1.0f, 0.0f}, .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f }, .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f }, .D_max = 2.2f, .offset = -0.05f, .gamma = 5.0f, .soft_clip = 0.75f, .exposure = 1.f, .black = 0.0755f };

dt_gui_presets_add_generic(_("black and white film"), self->op, self->version(), &tmq, sizeof(tmq), 1, DEVELOP_BLEND_CS_RGB_DISPLAY); }

void init_global(dt_iop_module_so_t *self) { dt_iop_negadoctor_global_data_t *gd = malloc(sizeof(dt_iop_negadoctor_global_data_t));

self->data = gd; const int program = 30; // negadoctor.cl, from programs.conf gd->kernel_negadoctor = dt_opencl_create_kernel(program, "negadoctor"); }

void cleanup_global(dt_iop_module_so_t *self) { dt_iop_negadoctor_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_negadoctor); free(self->data); self->data = NULL; }

void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = g_malloc0(sizeof(dt_iop_negadoctor_data_t)); }

void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { g_free(piece->data); piece->data = NULL; }

/* Global GUI stuff */

static void setup_color_variables(dt_iop_negadoctor_gui_data_t *const g, const gint state) { gtk_widget_set_visible(g->Dmin_G, state); gtk_widget_set_visible(g->Dmin_B, state); }

static void toggle_stock_controls(dt_iop_module_t *const self) { dt_iop_negadoctor_gui_data_t *const g = self->gui_data; const dt_iop_negadoctor_params_t *const p = self->params;

if(p->film_stock == DT_FILMSTOCK_NB) { // Hide color controls setup_color_variables(g, FALSE); dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min")); } else if(p->film_stock == DT_FILMSTOCK_COLOR) { // Show color controls setup_color_variables(g, TRUE); dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min red component")); } else { // We shouldn't be there dt_print(DT_DEBUG_ALWAYS, "negadoctor film stock: undefined behavior"); } }

static void Dmin_picker_update(dt_iop_module_t *self) { dt_iop_negadoctor_gui_data_t *const g = self->gui_data; const dt_iop_negadoctor_params_t *const p = self->params;

GdkRGBA color; color.alpha = 1.0f;

if(p->film_stock == DT_FILMSTOCK_COLOR) { color.red = p->Dmin[0]; color.green = p->Dmin[1]; color.blue = p->Dmin[2]; } else if(p->film_stock == DT_FILMSTOCK_NB) { color.red = color.green = color.blue = p->Dmin[0]; }

gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->Dmin_picker), &color); }

static void Dmin_picker_callback(GtkColorButton *widget, dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_iop_color_picker_reset(self, TRUE);

GdkRGBA c; gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c); p->Dmin[0] = c.red; p->Dmin[1] = c.green; p->Dmin[2] = c.blue;

++darktable.gui->reset; dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]); dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]); dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]); --darktable.gui->reset;

Dmin_picker_update(self); dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item(darktable.develop, self, TRUE); }

static void WB_low_picker_update(dt_iop_module_t *self) { dt_iop_negadoctor_gui_data_t *const g = self->gui_data; const dt_iop_negadoctor_params_t *const p = self->params;

GdkRGBA color; color.alpha = 1.0f;

dt_aligned_pixel_t WB_low_invert; for(size_t c = 0; c < 3; ++c) WB_low_invert[c] = 2.0f - p->wb_low[c]; const float WB_low_max = v_maxf(WB_low_invert); for(size_t c = 0; c < 3; ++c) WB_low_invert[c] /= WB_low_max;

color.red = WB_low_invert[0]; color.green = WB_low_invert[1]; color.blue = WB_low_invert[2];

gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->WB_low_picker), &color); }

static void WB_low_picker_callback(GtkColorButton *widget, dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_iop_color_picker_reset(self, TRUE);

GdkRGBA c; gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c);

dt_aligned_pixel_t RGB = { 2.0f - c.red, 2.0f - c.green, 2.0f - c.blue };

float RGB_min = v_minf(RGB); for(size_t k = 0; k < 3; k++) p->wb_low[k] = RGB[k] / RGB_min; p->wb_low[3] = 1.0f;

++darktable.gui->reset; dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]); dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]); dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]); --darktable.gui->reset;

WB_low_picker_update(self); dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item(darktable.develop, self, TRUE); }

static void WB_high_picker_update(dt_iop_module_t *self) { dt_iop_negadoctor_gui_data_t *const g = self->gui_data; const dt_iop_negadoctor_params_t *const p = self->params;

GdkRGBA color; color.alpha = 1.0f;

dt_aligned_pixel_t WB_high_invert; for(size_t c = 0; c < 3; ++c) WB_high_invert[c] = 2.0f - p->wb_high[c]; const float WB_high_max = v_maxf(WB_high_invert); for(size_t c = 0; c < 3; ++c) WB_high_invert[c] /= WB_high_max;

color.red = WB_high_invert[0]; color.green = WB_high_invert[1]; color.blue = WB_high_invert[2];

gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->WB_high_picker), &color); }

static void WB_high_picker_callback(GtkColorButton *widget, dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_iop_color_picker_reset(self, TRUE);

GdkRGBA c; gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c);

dt_aligned_pixel_t RGB = { 2.0f - c.red, 2.0f - c.green, 2.0f - c.blue }; float RGB_min = v_minf(RGB); for(size_t k = 0; k < 3; k++) p->wb_high[k] = RGB[k] / RGB_min; p->wb_high[3] = 1.0f;

++darktable.gui->reset; dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]); dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]); dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]); --darktable.gui->reset;

WB_high_picker_update(self); dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item(darktable.develop, self, TRUE); }

/* Color pickers auto-tuners */

// measure Dmin from the film edges first static void apply_auto_Dmin(dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

for(int k = 0; k < 4; k++) p->Dmin[k] = self->picked_color[k];

++darktable.gui->reset; dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]); dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]); dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]); --darktable.gui->reset;

Dmin_picker_update(self); dt_control_queue_redraw_widget(self->widget); dt_dev_add_history_item(darktable.develop, self, TRUE); }

// from Dmin, find out the range of density values of the film and compute Dmax static void apply_auto_Dmax(dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_aligned_pixel_t RGB; for(int c = 0; c < 3; c++) { RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD)); }

// Take the max(RGB) for safety. Big values unclip whites p->D_max = v_maxf(RGB);

++darktable.gui->reset; dt_bauhaus_slider_set(g->D_max, p->D_max); --darktable.gui->reset;

dt_control_queue_redraw_widget(self->widget); dt_dev_add_history_item(darktable.develop, self, TRUE); }

// from Dmax, compute the offset so the range of density is rescaled between [0; 1] static void apply_auto_offset(dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_aligned_pixel_t RGB; for(int c = 0; c < 3; c++) RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)) / p->D_max;

// Take the min(RGB) for safety. Negative values unclip blacks p->offset = v_minf(RGB);

++darktable.gui->reset; dt_bauhaus_slider_set(g->offset, p->offset); --darktable.gui->reset;

dt_control_queue_redraw_widget(self->widget); dt_dev_add_history_item(darktable.develop, self, TRUE); }

// from Dmax and offset, compute the white balance correction as multipliers of the offset // such that offset × wb[c] make black monochrome static void apply_auto_WB_low(dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_aligned_pixel_t RGB_min; for(int c = 0; c < 3; c++) RGB_min[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)) / p->D_max;

const float RGB_v_min = v_minf(RGB_min); // warning: can be negative for(int c = 0; c < 3; c++) p->wb_low[c] = RGB_v_min / RGB_min[c]; p->wb_low[3] = 1.0f;

++darktable.gui->reset; dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]); dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]); dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]); --darktable.gui->reset;

WB_low_picker_update(self); dt_control_queue_redraw_widget(self->widget); dt_dev_add_history_item(darktable.develop, self, TRUE); }

// from Dmax, offset and white balance multipliers, compute the white balance of the illuminant as multipliers of 1/Dmax // such that WB[c] / Dmax make white monochrome static void apply_auto_WB_high(dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_aligned_pixel_t RGB_min; for(int c = 0; c < 3; c++) 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));

const float RGB_v_min = v_minf(RGB_min); // warning : must be positive for(int c = 0; c < 3; c++) p->wb_high[c] = RGB_min[c] / RGB_v_min; p->wb_high[3] = 1.0f;

++darktable.gui->reset; dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]); dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]); dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]); --darktable.gui->reset;

WB_high_picker_update(self); dt_control_queue_redraw_widget(self->widget); dt_dev_add_history_item(darktable.develop, self, TRUE); }

// from Dmax, offset and both white balances, compute the print black adjustment // such that the printed values range from 0 to + infinity static void apply_auto_black(dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_aligned_pixel_t RGB; for(int c = 0; c < 3; c++) { RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)); RGB[c] *= p->wb_high[c] / p->D_max; RGB[c] += p->wb_low[c] * p->offset * p->wb_high[c]; RGB[c] = 0.1f - (1.0f - fast_exp10f(RGB[c])); // actually, remap between -3.32 EV and infinity for safety because gamma comes later } p->black = v_maxf(RGB);

++darktable.gui->reset; dt_bauhaus_slider_set(g->black, p->black); --darktable.gui->reset;

dt_control_queue_redraw_widget(self->widget); dt_dev_add_history_item(darktable.develop, self, TRUE); }

// from Dmax, offset, both white balances, and printblack, compute the print exposure adjustment as a scaling factor // such that the printed values range from 0 to 1 static void apply_auto_exposure(dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data; dt_iop_negadoctor_params_t *p = self->params;

dt_aligned_pixel_t RGB; for(int c = 0; c < 3; c++) { RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD)); RGB[c] *= p->wb_high[c] / p->D_max; RGB[c] += p->wb_low[c] * p->offset; RGB[c] = 0.96f / (1.0f - fast_exp10f(RGB[c]) + p->black); // actually, remap in [0; 0.96] for safety } p->exposure = v_minf(RGB);

++darktable.gui->reset; dt_bauhaus_slider_set(g->exposure, log2f(p->exposure)); --darktable.gui->reset;

dt_control_queue_redraw_widget(self->widget); dt_dev_add_history_item(darktable.develop, self, TRUE); }

void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_t *pipe) { if(darktable.gui->reset) return; dt_iop_negadoctor_gui_data_t *g = self->gui_data;

if (picker == g->Dmin_sampler) apply_auto_Dmin(self); else if(picker == g->WB_high_sampler) apply_auto_WB_high(self); else if(picker == g->offset) apply_auto_offset(self); else if(picker == g->D_max) apply_auto_Dmax(self); else if(picker == g->WB_low_sampler) apply_auto_WB_low(self); else if(picker == g->exposure) apply_auto_exposure(self); else if(picker == g->black) apply_auto_black(self); else dt_print(DT_DEBUG_ALWAYS, "[negadoctor] unknown color picker"); }

void gui_init(dt_iop_module_t *self) { dt_iop_negadoctor_gui_data_t *g = IOP_GUI_ALLOC(negadoctor);

static dt_action_def_t notebook_def = { }; g->notebook = dt_ui_notebook_new(&notebook_def); dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), &notebook_def);

// Page FILM PROPERTIES GtkWidget *page1 = self->widget = dt_ui_notebook_page(g->notebook, N_("film properties"), NULL);

// Dmin

gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(C_("section", "color of the film base")), FALSE, FALSE, 0);

GtkWidget *row1 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));

g->Dmin_picker = gtk_color_button_new(); gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->Dmin_picker), FALSE); gtk_color_button_set_title(GTK_COLOR_BUTTON(g->Dmin_picker), _("select color of film material from a swatch")); gtk_box_pack_start(GTK_BOX(row1), GTK_WIDGET(g->Dmin_picker), TRUE, TRUE, 0); g_signal_connect(G_OBJECT(g->Dmin_picker), "color-set", G_CALLBACK(Dmin_picker_callback), self);

g->Dmin_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row1); gtk_widget_set_tooltip_text(g->Dmin_sampler , ("pick color of film material from image")); dt_action_define_iop(self, N("pickers"), N_("film material"), g->Dmin_sampler, &dt_action_def_toggle);

gtk_box_pack_start(GTK_BOX(page1), GTK_WIDGET(row1), FALSE, FALSE, 0);

g->Dmin_R = dt_bauhaus_slider_from_params(self, "Dmin[0]"); dt_bauhaus_slider_set_digits(g->Dmin_R, 4); dt_bauhaus_slider_set_format(g->Dmin_R, "%"); dt_bauhaus_slider_set_factor(g->Dmin_R, 100); dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min red component")); gtk_widget_set_tooltip_text(g->Dmin_R, _("adjust the color and shade of the film transparent base.\n" "this value depends on the film material, \n" "the chemical fog produced while developing the film,\n" "and the scanner white balance."));

g->Dmin_G = dt_bauhaus_slider_from_params(self, "Dmin[1]"); dt_bauhaus_slider_set_digits(g->Dmin_G, 4); dt_bauhaus_slider_set_format(g->Dmin_G, "%"); dt_bauhaus_slider_set_factor(g->Dmin_G, 100); dt_bauhaus_widget_set_label(g->Dmin_G, NULL, N_("D min green component")); gtk_widget_set_tooltip_text(g->Dmin_G, _("adjust the color and shade of the film transparent base.\n" "this value depends on the film material, \n" "the chemical fog produced while developing the film,\n" "and the scanner white balance."));

g->Dmin_B = dt_bauhaus_slider_from_params(self, "Dmin[2]"); dt_bauhaus_slider_set_digits(g->Dmin_B, 4); dt_bauhaus_slider_set_format(g->Dmin_B, "%"); dt_bauhaus_slider_set_factor(g->Dmin_B, 100); dt_bauhaus_widget_set_label(g->Dmin_B, NULL, N_("D min blue component")); gtk_widget_set_tooltip_text(g->Dmin_B, _("adjust the color and shade of the film transparent base.\n" "this value depends on the film material, \n" "the chemical fog produced while developing the film,\n" "and the scanner white balance."));

// D max and scanner bias

gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(C_("section", "dynamic range of the film")), FALSE, FALSE, 0);

g->D_max = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "D_max")); dt_bauhaus_slider_set_format(g->D_max, " dB"); gtk_widget_set_tooltip_text(g->D_max, _("maximum density of the film, corresponding to white after inversion.\n" "this value depends on the film specifications, the developing process,\n" "the dynamic range of the scene and the scanner exposure settings."));

gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(C_("section", "scanner exposure settings")), FALSE, FALSE, 0);

g->offset = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "offset")); dt_bauhaus_slider_set_format(g->offset, " dB"); gtk_widget_set_tooltip_text(g->offset, _("correct the exposure of the scanner, for all RGB channels,\n" "before the inversion, so blacks are neither clipped or too pale."));

// Page CORRECTIONS GtkWidget *page2 = self->widget = dt_ui_notebook_page(g->notebook, N_("corrections"), NULL);

// WB shadows gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(C_("section", "shadows color cast")), FALSE, FALSE, 0);

GtkWidget *row3 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));

g->WB_low_picker = gtk_color_button_new(); gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->WB_low_picker), FALSE); gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_low_picker), _("select color of shadows from a swatch")); gtk_box_pack_start(GTK_BOX(row3), GTK_WIDGET(g->WB_low_picker), TRUE, TRUE, 0); g_signal_connect(G_OBJECT(g->WB_low_picker), "color-set", G_CALLBACK(WB_low_picker_callback), self);

g->WB_low_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row3); gtk_widget_set_tooltip_text(g->WB_low_sampler, ("pick shadows color from image")); dt_action_define_iop(self, N("pickers"), N_("shadows"), g->WB_low_sampler, &dt_action_def_toggle);

gtk_box_pack_start(GTK_BOX(page2), GTK_WIDGET(row3), FALSE, FALSE, 0);

g->wb_low_R = dt_bauhaus_slider_from_params(self, "wb_low[0]"); dt_bauhaus_widget_set_label(g->wb_low_R, NULL, N_("shadows red offset")); gtk_widget_set_tooltip_text(g->wb_low_R, _("correct the color cast in shadows so blacks are\n" "truly achromatic. Setting this value before\n" "the highlights illuminant white balance will help\n" "recovering the global white balance in difficult cases."));

g->wb_low_G = dt_bauhaus_slider_from_params(self, "wb_low[1]"); dt_bauhaus_widget_set_label(g->wb_low_G, NULL, N_("shadows green offset")); gtk_widget_set_tooltip_text(g->wb_low_G, _("correct the color cast in shadows so blacks are\n" "truly achromatic. Setting this value before\n" "the highlights illuminant white balance will help\n" "recovering the global white balance in difficult cases."));

g->wb_low_B = dt_bauhaus_slider_from_params(self, "wb_low[2]"); dt_bauhaus_widget_set_label(g->wb_low_B, NULL, N_("shadows blue offset")); gtk_widget_set_tooltip_text(g->wb_low_B, _("correct the color cast in shadows so blacks are\n" "truly achromatic. Setting this value before\n" "the highlights illuminant white balance will help\n" "recovering the global white balance in difficult cases."));

// WB highlights gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(C_("section", "highlights white balance")), FALSE, FALSE, 0);

GtkWidget *row2 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));

g->WB_high_picker = gtk_color_button_new(); gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->WB_high_picker), FALSE); gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_high_picker), _("select color of illuminant from a swatch")); gtk_box_pack_start(GTK_BOX(row2), GTK_WIDGET(g->WB_high_picker), TRUE, TRUE, 0); g_signal_connect(G_OBJECT(g->WB_high_picker), "color-set", G_CALLBACK(WB_high_picker_callback), self);

g->WB_high_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row2); gtk_widget_set_tooltip_text(g->WB_high_sampler , ("pick illuminant color from image")); dt_action_define_iop(self, N("pickers"), N_("illuminant"), g->WB_high_sampler, &dt_action_def_toggle);

gtk_box_pack_start(GTK_BOX(page2), GTK_WIDGET(row2), FALSE, FALSE, 0);

g->wb_high_R = dt_bauhaus_slider_from_params(self, "wb_high[0]"); dt_bauhaus_widget_set_label(g->wb_high_R, NULL, N_("illuminant red gain")); gtk_widget_set_tooltip_text(g->wb_high_R, _("correct the color of the illuminant so whites are\n" "truly achromatic. Setting this value after\n" "the shadows color cast will help\n" "recovering the global white balance in difficult cases."));

g->wb_high_G = dt_bauhaus_slider_from_params(self, "wb_high[1]"); dt_bauhaus_widget_set_label(g->wb_high_G, NULL, N_("illuminant green gain")); gtk_widget_set_tooltip_text(g->wb_high_G, _("correct the color of the illuminant so whites are\n" "truly achromatic. Setting this value after\n" "the shadows color cast will help\n" "recovering the global white balance in difficult cases."));

g->wb_high_B = dt_bauhaus_slider_from_params(self, "wb_high[2]"); dt_bauhaus_widget_set_label(g->wb_high_B, NULL, N_("illuminant blue gain")); gtk_widget_set_tooltip_text(g->wb_high_B, _("correct the color of the illuminant so whites are\n" "truly achromatic. Setting this value after\n" "the shadows color cast will help\n" "recovering the global white balance in difficult cases."));

// Page PRINT PROPERTIES GtkWidget *page3 = self->widget = dt_ui_notebook_page(g->notebook, N_("print properties"), NULL);

// print corrections gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(C_("section", "virtual paper properties")), FALSE, FALSE, 0);

g->black = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "black")); dt_bauhaus_slider_set_digits(g->black, 4); dt_bauhaus_slider_set_factor(g->black, 100); dt_bauhaus_slider_set_format(g->black, "%"); gtk_widget_set_tooltip_text(g->black, _("correct the density of black after the inversion,\n" "to adjust the global contrast while avoiding clipping shadows."));

g->gamma = dt_bauhaus_slider_from_params(self, "gamma"); dt_bauhaus_widget_set_label(g->gamma, NULL, N_("paper grade (gamma)")); gtk_widget_set_tooltip_text(g->gamma, _("select the grade of the virtual paper, which is actually\n" "equivalent to applying a gamma. it compensates the film D max\n" "and recovers the contrast. use a high grade for high D max."));

g->soft_clip = dt_bauhaus_slider_from_params(self, "soft_clip"); dt_bauhaus_slider_set_factor(g->soft_clip, 100); dt_bauhaus_slider_set_digits(g->soft_clip, 4); dt_bauhaus_slider_set_format(g->soft_clip, "%"); gtk_widget_set_tooltip_text(g->soft_clip, _("gradually compress specular highlights past this value\n" "to avoid clipping while pushing the exposure for mid-tones.\n" "this somewhat reproduces the behavior of matte paper."));

gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(C_("section", "virtual print emulation")), FALSE, FALSE, 0);

g->exposure = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "exposure")); dt_bauhaus_slider_set_hard_min(g->exposure, -1.0); dt_bauhaus_slider_set_soft_min(g->exposure, -1.0); dt_bauhaus_slider_set_hard_max(g->exposure, 1.0); dt_bauhaus_slider_set_format(g->exposure, _(" EV")); gtk_widget_set_tooltip_text(g->exposure, _("correct the printing exposure after inversion to adjust\n" "the global contrast and avoid clipping highlights."));

// start building top level widget self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE);

// Film emulsion g->film_stock = dt_bauhaus_combobox_from_params(self, "film_stock"); gtk_widget_set_tooltip_text(g->film_stock, _("toggle on or off the color controls"));

gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0); }

void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) { dt_iop_negadoctor_params_t *p = self->params; dt_iop_negadoctor_gui_data_t *g = self->gui_data; if(!w || w == g->film_stock) { toggle_stock_controls(self); Dmin_picker_update(self); } else if(w == g->Dmin_R && p->film_stock == DT_FILMSTOCK_NB) { dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[0]); dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[0]); } else if(w == g->Dmin_R || w == g->Dmin_G || w == g->Dmin_B) { Dmin_picker_update(self); } else if(w == g->exposure) { p->exposure = powf(2.0f, p->exposure); }

if(!w || w == g->wb_high_R || w == g->wb_high_G || w == g->wb_high_B) { WB_high_picker_update(self); }

if(!w || w == g->wb_low_R || w == g->wb_low_G || w == g->wb_low_B) { WB_low_picker_update(self); } }

void gui_update(dt_iop_module_t *const self) { // let gui slider match current parameters: dt_iop_negadoctor_gui_data_t *const g = self->gui_data; const dt_iop_negadoctor_params_t *const p = self->params;

dt_iop_color_picker_reset(self, TRUE);

dt_bauhaus_slider_set(g->exposure, log2f(p->exposure)); // warning: GUI is in EV dt_bauhaus_slider_set_default(g->exposure, log2f(p->exposure)); // otherwise always showes as "changed"

// Update custom stuff gui_changed(self, NULL, NULL); }

void gui_reset(dt_iop_module_t *self) { dt_iop_color_picker_reset(self, TRUE); } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on