diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1db6d20147f..4b3511eddc9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -133,6 +133,11 @@ changes (where available). - Escape now cancels a mask shape that is being drawn, as a right-click does. +- The histogram, waveform and RGB parade have a new button to switch to + an HDR scale, showing the image as the PQ or HLG signal of an HDR + export. The PQ scale is marked in nits and the HLG scale in percent + of signal, with reference white marked on both. + - Mask nodes close or outside the image boundaries are now editable. When "edit shapes on canvas" is enabled, the node editing area is extended beyond the image borders to allow editing of these nodes. diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in index 5a560846b10..0a41bb66659 100644 --- a/data/darktableconfig.xml.in +++ b/data/darktableconfig.xml.in @@ -3546,6 +3546,19 @@ + + plugins/darkroom/histogram/hdr_scale + + + + + + + + SDR + scale for histogram, waveform and RGB parade + show the histogram, waveform and RGB parade as an SDR signal, or as the PQ or HLG signal of an HDR export + plugins/darkroom/histogram/orient diff --git a/src/common/iop_profile.c b/src/common/iop_profile.c index 442a11d2f35..1fed3d89281 100644 --- a/src/common/iop_profile.c +++ b/src/common/iop_profile.c @@ -815,9 +815,17 @@ void dt_ioppr_set_thread_pq_peak_nits(const int nits) _thread_pq_peak_nits = nits; } -static float _output_headroom(const dt_colorspaces_color_profile_type_t type) +// the transfer of the darkroom scopes' HDR scale, DT_HDR_NONE when off +static int _scopes_hdr_transfer = DT_HDR_NONE; + +void dt_ioppr_set_scopes_hdr_transfer(const int transfer) +{ + _scopes_hdr_transfer = transfer; +} + +static float _transfer_headroom(const int transfer) { - switch(dt_colorspaces_hdr_transfer(type)) + switch(transfer) { case DT_HDR_PQ: { @@ -863,7 +871,12 @@ dt_ioppr_set_pipe_output_profile_info(struct dt_develop_t *dev, profile_info = dt_ioppr_add_profile_info_to_list(dev, DT_COLORSPACE_SRGB, "", intent); } pipe->output_profile_info = profile_info; - pipe->output_headroom = _output_headroom(profile_info->type); + pipe->output_headroom = _transfer_headroom(dt_colorspaces_hdr_transfer(profile_info->type)); + // the scopes read the preview pipe, and on an HDR scale they show what an + // export would encode, so the preview is tone mapped for the export headroom + if(dev && pipe == dev->preview_pipe) + pipe->output_headroom = fmaxf(pipe->output_headroom, + _transfer_headroom(_scopes_hdr_transfer)); return profile_info; } diff --git a/src/common/iop_profile.h b/src/common/iop_profile.h index 13f28f0c421..a0e00611394 100644 --- a/src/common/iop_profile.h +++ b/src/common/iop_profile.h @@ -120,6 +120,11 @@ dt_ioppr_set_pipe_output_profile_info(struct dt_develop_t *dev, or 0 to read it from the export module's setting again */ void dt_ioppr_set_thread_pq_peak_nits(const int nits); +/** set the HDR transfer (DT_HDR_PQ, DT_HDR_HLG_REC2020 or DT_HDR_NONE) shown + by the darkroom scopes; the preview pipe takes the output headroom of an + export with that transfer when it is larger than the display's */ +void dt_ioppr_set_scopes_hdr_transfer(const int transfer); + /** Record the export profile, used as a cache-identity tag. Returns the (possibly NULL) interned profile-info pointer, no fallback for sRGB in case of failure. */ dt_iop_order_iccprofile_info_t * diff --git a/src/dtgtk/paint.c b/src/dtgtk/paint.c index 1a5bf0427d8..05f3e7429e0 100644 --- a/src/dtgtk/paint.c +++ b/src/dtgtk/paint.c @@ -1472,6 +1472,23 @@ void dtgtk_cairo_paint_logarithmic_scale(cairo_t *cr, const gint x, const gint y FINISH } +void dtgtk_cairo_paint_hdr_scale(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, gint flags, void *data) +{ + PREAMBLE(1, 1, 0, 0) + + // reference white + cairo_move_to(cr, 0.0, 0.55); + cairo_line_to(cr, 1.0, 0.55); + cairo_stroke(cr); + + // tone curve with highlights going past it + cairo_move_to(cr, 0.0, 1.0); + cairo_curve_to(cr, 0.45, 1.0, 0.6, 0.55, 1.0, 0.0); + cairo_stroke(cr); + + FINISH +} + void dtgtk_cairo_paint_waveform_overlaid(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, gint flags, void *data) { PREAMBLE(1, 1, 0, 0) diff --git a/src/dtgtk/paint.h b/src/dtgtk/paint.h index 369d0e8ac00..49aa12f27ef 100644 --- a/src/dtgtk/paint.h +++ b/src/dtgtk/paint.h @@ -236,6 +236,8 @@ void dtgtk_cairo_paint_split_waveform_vectorscope(cairo_t *cr, gint x, gint y, g void dtgtk_cairo_paint_linear_scale(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data); /** paint logarithmic scale icon */ void dtgtk_cairo_paint_logarithmic_scale(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data); +/** paint HDR scale icon */ +void dtgtk_cairo_paint_hdr_scale(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data); /** paint waveform overlaid icon */ void dtgtk_cairo_paint_waveform_overlaid(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data); /** paint RGB parade icon */ diff --git a/src/libs/histogram.c b/src/libs/histogram.c index 0d89deee979..062c59260ff 100644 --- a/src/libs/histogram.c +++ b/src/libs/histogram.c @@ -19,6 +19,8 @@ #include "common/darktable.h" #include "common/color_picker.h" +#include "common/hdr-transfer.h" +#include "common/iop_profile.h" #include "gui/accelerators.h" #include "scopes.h" #include "scopes/vectorscope.h" @@ -78,6 +80,16 @@ int position(const dt_lib_module_t *self) return 1000; } +// stored in conf, so not translated +static const gchar *_hdr_scale_names[DT_SCOPES_HDR_N] = { "SDR", "PQ", "HLG" }; + +static gboolean _hdr_scale_available(const dt_scopes_t *const s) +{ + return s->cur_mode == &s->modes[DT_SCOPES_MODE_WAVEFORM] + || s->cur_mode == &s->modes[DT_SCOPES_MODE_PARADE] + || s->cur_mode == &s->modes[DT_SCOPES_MODE_HISTOGRAM]; +} + static void _scope_process (struct dt_lib_module_t *self, const float *const input, @@ -178,12 +190,27 @@ static void _scope_process dt_ioppr_add_profile_info_to_list(darktable.develop, DT_COLORSPACE_LIN_REC2020, "", DT_INTENT_RELATIVE_COLORIMETRIC); - const dt_iop_order_iccprofile_info_t *profile_info_out = !profile_info_to ? fallback : profile_info_to; + // an HDR scale shows what a PQ or HLG Rec2020 export would encode, so + // it ignores the histogram profile; the vectorscope (and so split) keeps it + const dt_scopes_hdr_scale_t hdr = + _hdr_scale_available(s) ? s->hdr_scale : DT_SCOPES_HDR_NONE; + + const dt_iop_order_iccprofile_info_t *profile_info_out = + !profile_info_to || hdr ? fallback : profile_info_to; dt_ioppr_transform_image_colorspace_rgb(input, img_display, width, height, profile_info_from, profile_info_out, "final histogram"); + if(hdr) + { + const int transfer = hdr == DT_SCOPES_HDR_PQ ? DT_HDR_PQ : DT_HDR_HLG_REC2020; + DT_OMP_FOR() + for(size_t k = 0; k < (size_t)4 * width * height; k += 4) + dt_hdr_encode(img_display + k, transfer); + } + dt_pthread_mutex_lock(&s->lock); + s->hdr_active = hdr; s->update_counter++; // if using a non-rgb profile_info_out as in cmyk softproofing we pass @@ -213,6 +240,68 @@ void lib_histogram_draw_bkgd(const dt_scopes_mode_t *const self, cairo_restore(cr); } +void lib_histogram_draw_hdr_grid(cairo_t *cr, + const int width, + const int height, + const gboolean values_on_y, + const dt_scopes_hdr_scale_t scale) +{ + // PQ is labeled in nits, as on HDR grading scopes. HLG is relative to + // the display peak, so it is labeled in percent of signal; BT.2408 puts + // reference white at 203 nits in PQ and at 75% signal in HLG + static const float pq_nits[] = { 1.f, 10.f, 100.f, DT_HDR_REFERENCE_WHITE_NITS, + 1000.f, 4000.f }; + static const float hlg_signal[] = { 0.25f, 0.5f, 0.75f }; + const gboolean pq = scale == DT_SCOPES_HDR_PQ; + const int num = pq ? sizeof(pq_nits) / sizeof(pq_nits[0]) + : sizeof(hlg_signal) / sizeof(hlg_signal[0]); + const double dashes = 4.0; + + PangoFontDescription *desc = dt_gui_get_font(); + pango_font_description_set_absolute_size(desc, DT_PIXEL_APPLY_DPI(9) * PANGO_SCALE); + PangoLayout *layout = pango_cairo_create_layout(cr); + pango_layout_set_font_description(layout, desc); + + cairo_save(cr); + const double wd = cairo_get_line_width(cr); + for(int k = 0; k < num; k++) + { + const float pos = pq ? dt_hdr_pq_encode(pq_nits[k] / DT_HDR_REFERENCE_WHITE_NITS) + : hlg_signal[k]; + const gboolean white = pq ? pq_nits[k] == DT_HDR_REFERENCE_WHITE_NITS + : hlg_signal[k] == 0.75f; + cairo_set_dash(cr, &dashes, white, 0); + cairo_set_line_width(cr, white ? wd * 3 : wd); + if(values_on_y) + dt_draw_line(cr, 0, (1.f - pos) * height, width, (1.f - pos) * height); + else + dt_draw_line(cr, pos * width, 0, pos * width, height); + cairo_stroke(cr); + + char text[16]; + if(pq) + snprintf(text, sizeof(text), "%.0f", pq_nits[k]); + else + snprintf(text, sizeof(text), "%.0f%%", 100.f * hlg_signal[k]); + PangoRectangle ink; + pango_layout_set_text(layout, text, -1); + pango_layout_get_pixel_extents(layout, &ink, NULL); + const int pad = DT_PIXEL_APPLY_DPI(2); + cairo_save(cr); + set_color(cr, darktable.bauhaus->graph_fg); + if(values_on_y) + cairo_move_to(cr, pad - ink.x, (1.f - pos) * height - ink.height - ink.y - pad); + else + cairo_move_to(cr, pos * width + pad - ink.x, pad - ink.y); + pango_cairo_show_layout(cr, layout); + cairo_restore(cr); + } + cairo_restore(cr); + + pango_font_description_free(desc); + g_object_unref(layout); +} + // FIXME: have different drawable for each scope in a stack -- // simplifies this function from being a swath of conditionals -- then // essentially draw callbacks _lib_histogram_draw_waveform, @@ -412,6 +501,61 @@ static void _drawable_leave(GtkEventControllerMotion *controller, } } +static void _hdr_scale_update_button(const dt_scopes_t *const s) +{ + static const char *tips[DT_SCOPES_HDR_N] = + { N_("SDR scale\nclick for an HDR scale showing a PQ signal,\n" + "from 0 to 10000 nits with 203 nits as reference white"), + N_("HDR scale: PQ signal, from 0 to 10000 nits\n" + "click for an HDR scale showing an HLG signal"), + N_("HDR scale: HLG signal, in percent of the display peak,\n" + "with reference white at 75 percent\nclick for the SDR scale") }; + gtk_widget_set_tooltip_text(s->hdr_button, _(tips[s->hdr_scale])); + // the button is shown pressed while an HDR scale is on + g_signal_handlers_block_matched(s->hdr_button, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, (gpointer)s); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->hdr_button), + s->hdr_scale != DT_SCOPES_HDR_NONE); + g_signal_handlers_unblock_matched(s->hdr_button, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, (gpointer)s); +} + +// the transfer last handed to the preview pipe +static int _pipe_hdr_transfer = DT_HDR_NONE; + +// the preview pipe is tone mapped for the headroom of the HDR scale shown, +// so returns TRUE when that changed and the preview has to be reprocessed +static gboolean _hdr_scale_set_pipe(const dt_scopes_t *const s) +{ + const dt_scopes_hdr_scale_t hdr = + _hdr_scale_available(s) ? s->hdr_scale : DT_SCOPES_HDR_NONE; + const int transfer = hdr == DT_SCOPES_HDR_PQ ? DT_HDR_PQ + : hdr == DT_SCOPES_HDR_HLG ? DT_HDR_HLG_REC2020 + : DT_HDR_NONE; + if(transfer == _pipe_hdr_transfer) return FALSE; + _pipe_hdr_transfer = transfer; + dt_ioppr_set_scopes_hdr_transfer(transfer); + return TRUE; +} + +static void _hdr_scale_toggle(GtkWidget *button, + dt_scopes_t *s) +{ + dt_pthread_mutex_lock(&s->lock); + s->hdr_scale = (s->hdr_scale + 1) % DT_SCOPES_HDR_N; + // the data on hand was binned for the other scale, so don't draw it + s->update_counter++; + dt_pthread_mutex_unlock(&s->lock); + dt_conf_set_string("plugins/darkroom/histogram/hdr_scale", + _hdr_scale_names[s->hdr_scale]); + _hdr_scale_update_button(s); + dt_scopes_refresh(s); + if(_hdr_scale_set_pipe(s) && dt_view_get_current() == DT_VIEW_DARKROOM) + dt_dev_reprocess_preview(darktable.develop, 0); + else + dt_scopes_reprocess(); +} + static void _mode_toggle(GtkWidget *button, dt_scopes_t *s) { // create radio-button-like behavior for choosing scope modes with a @@ -450,6 +594,7 @@ static void _mode_toggle(GtkWidget *button, dt_scopes_t *s) dt_scopes_call(prior_mode, mode_leave); gtk_widget_set_visible(s->button_box_rgb, dt_scopes_func_exists(s->cur_mode, draw_scope_channels)); + gtk_widget_set_visible(s->hdr_button, _hdr_scale_available(s)); dt_scopes_call(s->cur_mode, update_buttons); dt_scopes_call(s->cur_mode, mode_enter); @@ -457,7 +602,9 @@ static void _mode_toggle(GtkWidget *button, dt_scopes_t *s) dt_scopes_refresh(s); // FIXME: does this comparison of update_counter need to be protected within a // mutex? - if(s->update_counter != s->cur_mode->update_counter) + if(_hdr_scale_set_pipe(s) && dt_view_get_current() == DT_VIEW_DARKROOM) + dt_dev_reprocess_preview(darktable.develop, 0); + else if(s->update_counter != s->cur_mode->update_counter) dt_scopes_reprocess(); } @@ -595,6 +742,7 @@ static void _eventbox_enter_notify_callback(GtkEventControllerMotion *controller dt_scopes_call(s->cur_mode, mode_enter); gtk_widget_set_visible(s->button_box_rgb, dt_scopes_func_exists(s->cur_mode, draw_scope_channels)); + gtk_widget_set_visible(s->hdr_button, _hdr_scale_available(s)); gtk_widget_show(s->button_box_left); gtk_widget_show(s->button_box_right); } @@ -748,6 +896,11 @@ void gui_init(dt_lib_module_t *self) = dt_conf_get_bool("plugins/darkroom/histogram/show_green"); s->channels[DT_SCOPES_RGB_BLUE] = dt_conf_get_bool("plugins/darkroom/histogram/show_blue"); + const char *hdr_str = dt_conf_get_string_const("plugins/darkroom/histogram/hdr_scale"); + s->hdr_scale = DT_SCOPES_HDR_NONE; + for(dt_scopes_hdr_scale_t i = 0; i < DT_SCOPES_HDR_N; i++) + if(g_strcmp0(hdr_str, _hdr_scale_names[i]) == 0) + s->hdr_scale = i; // proxy functions and data so that pixelpipe or tether can // provide data for a histogram @@ -897,6 +1050,18 @@ void gui_init(dt_lib_module_t *self) g_signal_connect(G_OBJECT(btn), "toggled", G_CALLBACK(_channel_toggle), s); s->channel_buttons[i] = btn; } + s->hdr_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_hdr_scale, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .action = dark, + .action_label = N_("switch HDR scale"), + .action_def = &dt_action_def_button, + }); + gtk_widget_set_valign(s->hdr_button, GTK_ALIGN_START); + g_signal_connect(G_OBJECT(s->hdr_button), "toggled", G_CALLBACK(_hdr_scale_toggle), s); + _hdr_scale_update_button(s); + _hdr_scale_set_pipe(s); + dt_gui_box_add(s->button_box_right, s->hdr_button); + // RGB channels are always rightmost dt_gui_box_add(s->button_box_right, s->button_box_rgb); @@ -959,6 +1124,8 @@ void gui_cleanup(dt_lib_module_t *self) { dt_scopes_t *s = self->data; + _pipe_hdr_transfer = DT_HDR_NONE; + dt_ioppr_set_scopes_hdr_transfer(DT_HDR_NONE); for(dt_scopes_mode_type_t i = 0; i < DT_SCOPES_MODE_N; i++) dt_scopes_call(&s->modes[i], gui_cleanup); dt_pthread_mutex_destroy(&s->lock); diff --git a/src/libs/scopes.h b/src/libs/scopes.h index cf24c359500..39017521aad 100644 --- a/src/libs/scopes.h +++ b/src/libs/scopes.h @@ -53,6 +53,14 @@ typedef enum dt_scopes_rgb_t typedef gboolean scopes_channels_t[DT_SCOPES_RGB_N]; +typedef enum dt_scopes_hdr_scale_t +{ + DT_SCOPES_HDR_NONE = 0, + DT_SCOPES_HDR_PQ, + DT_SCOPES_HDR_HLG, + DT_SCOPES_HDR_N // needs to be the last one +} dt_scopes_hdr_scale_t; + struct dt_scopes_t; struct dt_scopes_mode_t; @@ -141,6 +149,8 @@ typedef struct dt_scopes_t int update_counter; // most recent pixelpipe vs mode data dt_scopes_highlight_t highlight; // depends on mouse position scopes_channels_t channels; // display state chosen by RGB buttons + dt_scopes_hdr_scale_t hdr_scale; // scale chosen by the HDR button + dt_scopes_hdr_scale_t hdr_active; // encoding of the data being shown gboolean dragging; // to block motion handling during drag gdouble last_offset_x, last_offset_y; // for drag handling // UI elements @@ -151,6 +161,7 @@ typedef struct dt_scopes_t GtkWidget *button_box_right; // GtkBox -- option buttons for main scope GtkWidget *button_box_rgb; // GtkBox -- RGB channels buttons GtkWidget *channel_buttons[DT_SCOPES_RGB_N]; // Array of GtkToggleButton -- RGB channels + GtkWidget *hdr_button; // GtkToggleButton -- SDR or HDR scale GtkWidget *scope_draw; // GtkDrawingArea -- scope & resize // for access to data during process/draw dt_pthread_mutex_t lock; @@ -171,6 +182,12 @@ extern void lib_histogram_draw_bkgd(const dt_scopes_mode_t *const self, const int height); // FIXME: is there any reason this needs to be called with an argument? extern void lib_histogram_update_tooltip(const dt_scopes_t *const scopes); +// grid for an HDR scale, with values running up the y axis or along the x axis +extern void lib_histogram_draw_hdr_grid(cairo_t *cr, + const int width, + const int height, + const gboolean values_on_y, + const dt_scopes_hdr_scale_t scale); #define dt_scopes_func_exists(mode, func) ((mode)->functions->func != NULL) diff --git a/src/libs/scopes/histogram.c b/src/libs/scopes/histogram.c index 5b529b8def4..2de38ac15e6 100644 --- a/src/libs/scopes/histogram.c +++ b/src/libs/scopes/histogram.c @@ -90,7 +90,10 @@ static void _hist_draw_grid(const dt_scopes_mode_t *const self, { // FIXME: now that vectorscope grid represents log scale, should // histogram grid do the same? - dt_draw_grid(cr, 4, 0, 0, width, height); + if(self->scopes->hdr_active) + lib_histogram_draw_hdr_grid(cr, width, height, FALSE, self->scopes->hdr_active); + else + dt_draw_grid(cr, 4, 0, 0, width, height); } static void _hist_draw_highlight(const dt_scopes_mode_t *const self, diff --git a/src/libs/scopes/waveform.c b/src/libs/scopes/waveform.c index 0fe6f4e2636..3e12f6842ef 100644 --- a/src/libs/scopes/waveform.c +++ b/src/libs/scopes/waveform.c @@ -78,6 +78,7 @@ static void _wave_process(dt_scopes_mode_t *const self, const size_t num_bins = ceilf(to_bin / (float)samples_per_bin); d->waveform_bins = num_bins; const size_t num_tones = d->waveform_tones; + const float top = self->scopes->hdr_active ? 1.0f : 8.0f / 9.0f; // Note that, with current constants, the input buffer is from the // preview pixelpipe and should be <= 1440x900x4. The output buffer @@ -99,8 +100,8 @@ static void _wave_process(dt_scopes_mode_t *const self, size_t tone[4] DT_ALIGNED_PIXEL; for_each_channel(ch, aligned(px,tone:16)) { - // 1.0 is at 8/9 of the height! - const float v = (8.0f / 9.0f) * px[4U * (x + roi->crop_x) + ch]; + // 1.0 is at 8/9 of the height, unless the data is an HDR signal + const float v = top * px[4U * (x + roi->crop_x) + ch]; // Using ceilf brings everything <= 0 to bottom tone, // everything > 1.0f/(num_tones-1) to top tone. tone[ch] = ceilf(CLAMPS(v, 0.0f, 1.0f) * (num_tones-1)); @@ -170,8 +171,12 @@ static void _wave_draw_grid(const dt_scopes_mode_t *const self, const int height) { const dt_scopes_wave_t *const d = self->data; - dt_draw_waveform_lines(cr, 0, 0, width, height, - d->orient == DT_WAVE_ORIENT_HORI); + if(self->scopes->hdr_active) + lib_histogram_draw_hdr_grid(cr, width, height, d->orient == DT_WAVE_ORIENT_HORI, + self->scopes->hdr_active); + else + dt_draw_waveform_lines(cr, 0, 0, width, height, + d->orient == DT_WAVE_ORIENT_HORI); } static void _wave_draw_highlight(const dt_scopes_mode_t *const self,