/*
This file is part of darktable,
Copyright (C) 2024-2026 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 .
*/
#include "bauhaus/bauhaus.h"
#include "common/interpolation.h"
#include "common/math.h"
#include "common/overlay.h"
#include "common/utility.h"
#include "control/control.h"
#include "develop/develop.h"
#include "develop/imageop.h"
#include "develop/imageop_gui.h"
#include "dtgtk/button.h"
#include "dtgtk/drawingarea.h"
#include "dtgtk/resetlabel.h"
#include "dtgtk/togglebutton.h"
#include "gui/accelerators.h"
#include "gui/drag_and_drop.h"
#include "gui/gtk.h"
#include "iop/iop_api.h"
#include
#include
#include
#include
DT_MODULE_INTROSPECTION(2, dt_iop_overlay_params_t)
// Compositing precision. Not exposed in the UI: every new edit uses the
// high-precision float path, while edits created before it existed are pinned
// to LEGACY by legacy_params() so their rendering never changes.
typedef enum dt_iop_overlay_compositing_t
{
DT_OVERLAY_COMPOSITE_HQ = 0,
DT_OVERLAY_COMPOSITE_LEGACY = 1
} dt_iop_overlay_compositing_t;
typedef enum dt_iop_overlay_base_scale_t
{
DT_SCALE_MAINMENU_IMAGE = 0, // $DESCRIPTION: "image"
DT_SCALE_MAINMENU_LARGER_BORDER = 1, // $DESCRIPTION: "larger border"
DT_SCALE_MAINMENU_SMALLER_BORDER = 2, // $DESCRIPTION: "smaller border"
DT_SCALE_MAINMENU_MARKERHEIGHT = 3, // $DESCRIPTION: "height"
DT_SCALE_MAINMENU_ADVANCED = 4 // $DESCRIPTION: "advanced options"
} dt_iop_overlay_base_scale_t; // this is the first
// drop-down menu, always
// visible
typedef enum dt_iop_overlay_img_scale_t
{
DT_SCALE_IMG_WIDTH = 1, // $DESCRIPTION: "image width"
DT_SCALE_IMG_HEIGHT = 2, // $DESCRIPTION: "image height"
DT_SCALE_IMG_LARGER = 3, // $DESCRIPTION: "larger image border"
DT_SCALE_IMG_SMALLER = 4, // $DESCRIPTION: "smaller image border"
} dt_iop_overlay_img_scale_t; // advanced drop-down no. 1
typedef enum dt_iop_overlay_svg_scale_t
{
DT_SCALE_SVG_WIDTH = 0, // $DESCRIPTION: "marker width"
DT_SCALE_SVG_HEIGHT = 1, // $DESCRIPTION: "marker height"
} dt_iop_overlay_svg_scale_t; // advanced drop-down no. 1
typedef struct dt_iop_overlay_params_t
{
/** opacity value of rendering overlay */
float opacity; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 100.0
/** scale value of rendering overlay */
float scale; // $MIN: 1.0 $MAX: 500.0 $DEFAULT: 100.0
/** Pixel independent xoffset, 0 to 1 */
float xoffset; // $MIN: -1.0 $MAX: 1.0, 0.001 $DEFAULT: 0.0 $DESCRIPTION: "x offset"
/** Pixel independent yoffset, 0 to 1 */
float yoffset; // $MIN: -1.0 $MAX: 1.0, 0.001 $DEFAULT: 0.0 $DESCRIPTION: "y offset"
/** Alignment value 0-8 3x3 */
int alignment; // $DEFAULT: 4
/** Rotation **/
float rotate; // $MIN: -180.0 $MAX: 180.0 $DEFAULT: 0.0 $DESCRIPTION: "rotation"
dt_iop_overlay_base_scale_t scale_base; // $DEFAULT: DT_SCALE_MAINMENU_IMAGE $DESCRIPTION: "scale on"
dt_iop_overlay_img_scale_t scale_img; // $DEFAULT: DT_SCALE_IMG_LARGER $DESCRIPTION: "scale marker to"
dt_iop_overlay_svg_scale_t scale_svg; // $DEFAULT: DT_SCALE_SVG_WIDTH $DESCRIPTION: "scale marker reference"
dt_imgid_t imgid; // overlay image id $DESCRIPTION: "image id"
char filename[1024]; // full overlay's filename
// compositing precision, not user-settable (see dt_iop_overlay_compositing_t)
dt_iop_overlay_compositing_t compositing; // $DEFAULT: DT_OVERLAY_COMPOSITE_HQ
// reserved (kept to allow future fields without a version bump)
size_t dummy1;
int64_t dummy2;
} dt_iop_overlay_params_t;
typedef struct dt_iop_overlay_data_t
{
float opacity;
float scale;
float xoffset;
float yoffset;
int alignment;
float rotate;
dt_iop_overlay_base_scale_t scale_base;
dt_iop_overlay_svg_scale_t scale_svg;
dt_iop_overlay_img_scale_t scale_img;
dt_imgid_t imgid;
char filename[1024];
dt_iop_overlay_compositing_t compositing;
} dt_iop_overlay_data_t;
#define MAX_OVERLAY 50
typedef struct dt_iop_overlay_global_data_t
{
// Cached overlay buffer, one slot per instance (index = multi_priority).
// The stored format depends on the instance's compositing mode:
// - HQ: 4-channel float in the pipe's scene-referred linear working RGB
// (colorout filtered out, gamma terminal but passing the float through),
// so compositing stays high-precision and colour-matched to the host pipe.
// - LEGACY: 8-bit Cairo ARGB32 (the original behaviour), kept for backward
// compatibility with edits made before the float path existed.
// cache_legacy[] records which format the slot currently holds, so a mode
// change invalidates and re-renders it.
void *cache[MAX_OVERLAY];
size_t cwidth[MAX_OVERLAY];
size_t cheight[MAX_OVERLAY];
gboolean cache_legacy[MAX_OVERLAY];
dt_pthread_mutex_t overlay_threadsafe;
int kernel_overlay_blend; // float RGBA blend (HQ)
int kernel_overlay_blend_legacy; // 8-bit Cairo ARGB blend (legacy)
} dt_iop_overlay_global_data_t;
typedef struct dt_iop_overlay_gui_data_t
{
GtkDrawingArea *area;
GtkWidget *align[9]; // Alignment buttons
GtkWidget *opacity, *scale, *x_offset, *y_offset; // opacity, scale, xoffs, yoffs
GtkWidget *scale_base; // "scale on"
GtkWidget *scale_img; // scale reference of image
GtkWidget *scale_svg; // scale reference of marker
GtkWidget *rotate;
GtkWidget *imgid;
gboolean drop_inside;
} dt_iop_overlay_gui_data_t;
/* Notes about the implementation.
The creation of the overlay image use a standard pipe run. This is
not fast so a cache is used.
- The cached overlay buffers are stored into the global data.
One slot is allocated for each instance (index is the multi_priority)
and holds buffer address and dimensions.
- To make the internal cache working safely we use a mutex encapsulating cache
buffer changes making process() re-entry safe for concurrent pixelpipe runs.
*/
const char *name()
{
return _("composite");
}
const char **description(dt_iop_module_t *self)
{
return dt_iop_set_description(self,
_("combine the image with elements from another processed image"),
_("corrective and creative"),
_("linear, RGB, scene-referred"),
_("linear, RGB"),
_("linear, RGB, scene-referred"));
}
const char *aliases()
{
return _("layer|stack|overlay");
}
int flags()
{
return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING;
}
int default_group()
{
return IOP_GROUP_EFFECT | IOP_GROUP_EFFECTS;
}
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;
}
static GList *_get_disabled_modules(const dt_iop_module_t *self,
const dt_imgid_t imgid)
{
const dt_develop_t *dev = self->dev;
const int multi_priority = self->multi_priority;
/* we want a list of all modules that are after the current
overlay module iop-order to ensure they are not processed via dt_dev_image().
There are some exceptions:
- gamma and finalscale are required
- crop and &ashift make sense
colorout is *not* an exception: keeping it filtered out (as it always was)
means the overlay is rendered in the pipe's scene-referred linear working
RGB. Combined with the want_float passthrough render in dt_dev_image(), the
overlay reaches process() as linear float instead of a posterized 8-bit
linear buffer, matching the host pipe at this module's position.
The list order does not matter
*/
const dt_iop_module_t *self_module = dt_iop_get_module_by_op_priority
(dev->iop, "overlay", multi_priority);
const gboolean is_current = dt_dev_is_current_image(dev, imgid);
GList *result = NULL;
gboolean after = FALSE;
for(GList *l = dev->iop; l; l = g_list_next(l))
{
dt_iop_module_t *mod = l->data;
if((after
&& !dt_iop_module_is_gamma(mod)
&& !dt_iop_module_is_finalscale(mod)
&& !dt_iop_module_is(mod, "crop")
&& !dt_iop_module_is(mod, "ashift"))
|| (is_current
&& ( dt_iop_module_is(mod, "overlay")
|| dt_iop_module_is(mod, "enlargecanvas"))))
{
result = g_list_prepend(result, mod->op);
}
// look for ourself, disable all modules after this point
if(dt_iop_module_is(mod, self_module->op)
&& mod->multi_priority == multi_priority)
after = TRUE;
}
if(darktable.unmuted & (DT_DEBUG_PARAMS | DT_DEBUG_PIPE))
{
char *buf = g_malloc0(PATH_MAX);
for(GList *m = result; m; m = g_list_next(m))
{
const char *mod = (char *)(m->data);
g_strlcat(buf, mod, PATH_MAX);
g_strlcat(buf, " ", PATH_MAX);
}
dt_print_pipe(DT_DEBUG_PARAMS | DT_DEBUG_PIPE, "module_filter_out",
NULL, self, DT_DEVICE_NONE, NULL, NULL,
"%s", buf);
g_free(buf);
}
return result;
}
static void _clear_cache_entry(const dt_iop_module_t *self, const int index)
{
dt_iop_overlay_global_data_t *gd = self->global_data;
if(!gd) return;
dt_free_align(gd->cache[index]);
gd->cache[index] = NULL;
}
static void _module_remove_callback(gpointer instance,
dt_iop_module_t *self,
const gpointer user_data)
{
if(!self || self != user_data) return;
const dt_iop_overlay_params_t *p = self->params;
if(dt_is_valid_imgid(p->imgid))
dt_overlay_remove(self->dev->image_storage.id, p->imgid);
}
static void _setup_overlay(dt_iop_module_t *self,
const dt_dev_pixelpipe_iop_t *piece,
const gboolean legacy,
void **pbuf,
size_t *pwidth,
size_t *pheight)
{
dt_iop_overlay_params_t *p = self->params;
const dt_iop_overlay_gui_data_t *g = self->gui_data;
const dt_iop_overlay_data_t *data = piece->data;
dt_imgid_t imgid = data->imgid;
if(!p || !dt_is_valid_imgid(imgid))
return;
dt_develop_t *dev = self->dev;
gboolean image_exists = dt_image_exists(imgid);
// The overlay image could have been removed from collection and
// imported again. Check if we can find
if(!image_exists)
{
const dt_imgid_t new_imgid = dt_image_get_id_full_path(data->filename);
if(dt_is_valid_imgid(new_imgid))
{
image_exists = TRUE;
p->imgid = new_imgid;
imgid = new_imgid;
dt_dev_add_history_item(dev, self, TRUE);
if(g)
gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
else if(g)
{
const gchar *tooltip = g_strdup_printf
(_("overlay image missing from database\n\n"
"'%s'" ), data->filename);
gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), tooltip);
}
}
if(image_exists)
{
if(g)
gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), "");
uint8_t *buf;
size_t bw;
size_t bh;
GList *disabled_modules = _get_disabled_modules(self, imgid);
// HQ (legacy == FALSE): render scene-referred linear float (want_float):
// colorout is filtered out by _get_disabled_modules() while gamma stays the
// terminal module and passes the 4-channel float straight through (see
// gamma.c process()), so the overlay arrives in the working RGB space rather
// than as an 8-bit ARGB display backbuf.
// LEGACY (legacy == TRUE): render the 8-bit ARGB display backbuf (want_float
// FALSE), the original behaviour that the Cairo compositing path expects.
const gboolean want_float = !legacy;
// Render at the parent image's storage resolution. The result is
// scale-stable and cached across host-pipe zoom changes; the float
// resampler in _get_overlay_rgba_f() handles any final downscaling at
// composite time. Rendering at a smaller,
// zoom-dependent target would change the output of scale-dependent
// modules in the overlay's pipeline (sharpen, denoise, local contrast,
// lens correction, ...) and break preview/export consistency.
const size_t width = dev->image_storage.width;
const size_t height = dev->image_storage.height;
dt_dev_image(imgid,
width,
height,
-1,
&buf,
NULL,
&bw,
&bh,
NULL,
-1,
disabled_modules,
piece->pipe->devid,
TRUE,
want_float);
void *old_buf = *pbuf;
*pwidth = bw;
*pheight = bh;
*pbuf = buf;
dt_free_align(old_buf);
}
else
{
dt_control_log(_("image %d does not exist"), imgid);
}
}
// Placement geometry shared by the HQ (float) and legacy (Cairo) compositors.
// All of this math is identical between the two paths; only the final
// resampling/blend differs.
typedef struct _overlay_geometry_t
{
float scale; // net scale, including roi_out->scale and the user scale
float tx, ty; // image-space translation (x/y offset folded in), pre roi_out->scale
float cX, cY; // rotation centre, in output (device) pixels
} _overlay_geometry_t;
static _overlay_geometry_t _overlay_compute_geometry(const dt_iop_overlay_data_t *data,
const dt_dev_pixelpipe_iop_t *piece,
const dt_iop_roi_t *const roi_out,
const size_t bw,
const size_t bh,
const float angle)
{
// overlay source dimensions (named 'dimension' so the placement math below
// stays verbatim from the original implementation)
struct
{
int width;
int height;
} dimension;
dimension.width = (int)bw ? (int)bw : 1;
dimension.height = (int)bh ? (int)bh : 1;
const float iw = piece->buf_in.width;
const float ih = piece->buf_in.height;
const float uscale = data->scale / 100.0f;
float wbase, hbase, scale, sbase;
const float larger = dimension.width > dimension.height
? (float)dimension.width : (float)dimension.height;
switch(data->scale_base)
{
case DT_SCALE_MAINMENU_LARGER_BORDER:
sbase = wbase = hbase = (iw > ih) ? iw : ih;
scale = sbase / larger;
break;
case DT_SCALE_MAINMENU_SMALLER_BORDER:
sbase = wbase = hbase = (iw < ih) ? iw : ih;
scale = sbase / larger;
break;
case DT_SCALE_MAINMENU_MARKERHEIGHT:
wbase = iw; sbase = hbase = ih;
scale = sbase / dimension.height;
break;
case DT_SCALE_MAINMENU_ADVANCED:
wbase = iw; hbase = ih;
if(data->scale_img == DT_SCALE_IMG_WIDTH)
{
sbase = iw;
scale = (data->scale_svg == DT_SCALE_SVG_WIDTH)
? sbase / dimension.width : sbase / dimension.height;
}
else if(data->scale_img == DT_SCALE_IMG_HEIGHT)
{
sbase = ih;
scale = (data->scale_svg == DT_SCALE_SVG_WIDTH)
? sbase / dimension.width : sbase / dimension.height;
}
else if(data->scale_img == DT_SCALE_IMG_LARGER)
{
sbase = (iw > ih) ? iw : ih;
scale = (data->scale_svg == DT_SCALE_SVG_WIDTH)
? sbase / dimension.width : sbase / dimension.height;
}
else
{
sbase = (iw < ih) ? iw : ih;
scale = (data->scale_svg == DT_SCALE_SVG_WIDTH)
? sbase / dimension.width : sbase / dimension.height;
}
break;
case DT_SCALE_MAINMENU_IMAGE:
default:
wbase = iw; hbase = ih;
scale = (dimension.width > dimension.height)
? iw / dimension.width : ih / dimension.height;
}
scale *= roi_out->scale;
scale *= uscale;
float svg_width, svg_height;
gboolean svg_calc_heightfromwidth;
float svg_calc_base;
switch(data->scale_base)
{
case DT_SCALE_MAINMENU_LARGER_BORDER:
svg_calc_base = ((iw > ih) ? iw : ih) * uscale;
svg_calc_heightfromwidth = (dimension.width > dimension.height);
break;
case DT_SCALE_MAINMENU_SMALLER_BORDER:
svg_calc_base = ((iw < ih) ? iw : ih) * uscale;
svg_calc_heightfromwidth = (dimension.width > dimension.height);
break;
case DT_SCALE_MAINMENU_MARKERHEIGHT:
svg_calc_base = ih * uscale;
svg_calc_heightfromwidth = FALSE;
break;
case DT_SCALE_MAINMENU_ADVANCED:
if(data->scale_img == DT_SCALE_IMG_WIDTH)
{
svg_calc_base = iw * uscale;
svg_calc_heightfromwidth = (data->scale_svg == DT_SCALE_SVG_WIDTH);
}
else if(data->scale_img == DT_SCALE_IMG_HEIGHT)
{
svg_calc_base = ih * uscale;
svg_calc_heightfromwidth = (data->scale_svg == DT_SCALE_SVG_WIDTH);
}
else if(data->scale_img == DT_SCALE_IMG_LARGER)
{
svg_calc_base = ((iw > ih) ? iw : ih) * uscale;
svg_calc_heightfromwidth = (data->scale_svg == DT_SCALE_SVG_WIDTH);
}
else
{
svg_calc_base = ((iw < ih) ? iw : ih) * uscale;
svg_calc_heightfromwidth = (data->scale_svg == DT_SCALE_SVG_WIDTH);
}
break;
case DT_SCALE_MAINMENU_IMAGE:
default:
if(dimension.width > dimension.height)
{
svg_calc_base = iw * uscale;
svg_calc_heightfromwidth = TRUE;
}
else
{
svg_calc_base = ih * uscale;
svg_calc_heightfromwidth = FALSE;
}
}
if(svg_calc_heightfromwidth)
{
svg_width = svg_calc_base;
svg_height = dimension.height * (svg_width / dimension.width);
}
else
{
svg_height = svg_calc_base;
svg_width = dimension.width * (svg_height / dimension.height);
}
const float bb_width = fabsf(svg_width * cosf(angle)) + fabsf(svg_height * sinf(angle));
const float bb_height = fabsf(svg_width * sinf(angle)) + fabsf(svg_height * cosf(angle));
const float bX = bb_width / 2.0f - svg_width / 2.0f;
const float bY = bb_height / 2.0f - svg_height / 2.0f;
float ty = 0.f, tx = 0.f;
if(data->alignment >= 0 && data->alignment < 3)
ty = bY;
else if(data->alignment >= 3 && data->alignment < 6)
ty = (ih / 2.0f) - (svg_height / 2.0f);
else if(data->alignment >= 6 && data->alignment < 9)
ty = ih - svg_height - bY;
if(data->alignment == 0 || data->alignment == 3 || data->alignment == 6)
tx = bX;
else if(data->alignment == 1 || data->alignment == 4 || data->alignment == 7)
tx = (iw / 2.0f) - (svg_width / 2.0f);
else if(data->alignment == 2 || data->alignment == 5 || data->alignment == 8)
tx = iw - svg_width - bX;
tx += data->xoffset * wbase;
ty += data->yoffset * hbase;
_overlay_geometry_t geo;
geo.scale = scale;
geo.tx = tx;
geo.ty = ty;
// Centre of rotation, in output (device) pixels. Matches the Cairo chain:
// translate(-roi_in) · translate(t*scale) · translate(c) · rotate · translate(-c) · scale
geo.cX = svg_width / 2.0f * roi_out->scale;
geo.cY = svg_height / 2.0f * roi_out->scale;
return geo;
}
/* Composite the overlay into a straight-alpha float RGBA buffer at roi_out
* dimensions, in the host pipe's scene-referred linear working RGB.
*
* The cached overlay (*pbuf) is rendered once at parent-image storage
* resolution and reused across zoom levels. Placement / scale / rotation are
* applied here with a two-stage float resampler: anti-aliased minification
* (dt_interpolation_resample) followed by per-pixel bicubic sampling of the
* rotated placement. This is the high-precision counterpart to the 8-bit Cairo
* path (_get_overlay_argb), which posterizes smooth gradients because the
* overlay is quantized to a linear 8-bit buffer before compositing; that path
* is kept selectable (DT_OVERLAY_COMPOSITE_LEGACY) for backward compatibility.
*
* Locking: overlay_threadsafe is held for the whole resample so the cached
* buffer stays stable while it is read.
*
* Returns a dt_alloc_align_float'd buffer of 4 * roi_out->width *
* roi_out->height floats (R, G, B, coverage per pixel) that the caller must
* dt_free_align(), or NULL on failure.
*/
static float *_get_overlay_rgba_f(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
dt_iop_overlay_data_t *data = piece->data;
dt_iop_overlay_global_data_t *gd = self->global_data;
const int index = self->multi_priority;
const float angle = deg2radf(-data->rotate);
// ── Acquire / refresh the overlay buffer ─────────────────────────────────
dt_pthread_mutex_lock(&gd->overlay_threadsafe);
const gboolean use_cache = (self->dev->image_storage.id == darktable.develop->image_storage.id);
void *cbuf = NULL;
size_t cwidth = 0;
size_t cheight = 0;
void **pbuf = use_cache ? &gd->cache[index] : &cbuf;
size_t *pwidth = use_cache ? &gd->cwidth[index] : &cwidth;
size_t *pheight = use_cache ? &gd->cheight[index] : &cheight;
if(!dt_is_valid_imgid(data->imgid))
_clear_cache_entry(self, index);
// drop a cached buffer left over from the other compositing mode
if(use_cache && gd->cache[index] && gd->cache_legacy[index])
_clear_cache_entry(self, index);
if(!*pbuf)
{
_setup_overlay(self, piece, FALSE /* legacy */, pbuf, pwidth, pheight);
if(use_cache)
gd->cache_legacy[index] = FALSE;
}
if(!*pbuf)
{
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
return NULL;
}
const size_t bw = *pwidth;
const size_t bh = *pheight;
const _overlay_geometry_t geo = _overlay_compute_geometry(data, piece, roi_out, bw, bh, angle);
const float scale = geo.scale;
const float tx = geo.tx;
const float ty = geo.ty;
const float cX = geo.cX;
const float cY = geo.cY;
// ── Two-stage float resample ─────────────────────────────────────────────
// Stage 1: anti-aliased minification of the source down to roughly its
// displayed footprint, so the per-pixel sampling below runs near 1:1 and does
// not alias on heavy downscales (storage resolution → preview).
const float *src = *pbuf;
size_t sw = bw;
size_t sh = bh;
float *mip = NULL;
if(scale < 1.0f)
{
const int mw = MAX(1, (int)roundf((float)bw * scale));
const int mh = MAX(1, (int)roundf((float)bh * scale));
mip = dt_alloc_align_float((size_t)4 * mw * mh);
if(mip)
{
const dt_iop_roi_t rin = { 0, 0, (int)bw, (int)bh, 1.0f };
const dt_iop_roi_t rout = { 0, 0, mw, mh, (float)mw / (float)bw };
const dt_interpolation_t *down = dt_interpolation_new(DT_INTERPOLATION_LANCZOS3);
dt_interpolation_resample(down, mip, &rout, *pbuf, &rin);
src = mip;
sw = mw;
sh = mh;
}
}
const int ow = roi_out->width;
const int oh = roi_out->height;
float *canvas = dt_alloc_align_float((size_t)4 * ow * oh);
if(!canvas)
{
dt_print(DT_DEBUG_ALWAYS, "[overlay] out of memory %d*%d", ow, oh);
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
dt_free_align(mip);
if(!use_cache)
dt_free_align(cbuf);
return NULL;
}
// Stage 2: inverse-map each output pixel back to source coordinates, applying
// the same translate / rotate / scale chain Cairo used, then sample.
const dt_interpolation_t *itor = dt_interpolation_new(DT_INTERPOLATION_BICUBIC);
const float ca = cosf(angle);
const float sa = sinf(angle);
const float t2x = tx * roi_out->scale;
const float t2y = ty * roi_out->scale;
const float msx = (float)sw / (float)bw; // mip scale (1.0 when no prefilter)
const float msy = (float)sh / (float)bh;
// dt_interpolation_compute_pixel4c expects the line stride in floats
// (elements), not bytes — it indexes a float* as `in + linestride * iy`.
const int src_stride = (int)(sw * 4);
const float fsw = (float)sw;
const float fsh = (float)sh;
DT_OMP_FOR(collapse(2))
for(int y = 0; y < oh; y++)
for(int x = 0; x < ow; x++)
{
// device → pre-scale frame (inverse translate/rotate)
const float dvx = (float)x + (float)roi_in->x - t2x - cX;
const float dvy = (float)y + (float)roi_in->y - t2y - cY;
const float rx = ca * dvx + sa * dvy; // inverse rotation (by -angle)
const float ry = -sa * dvx + ca * dvy;
// → full-res source coordinates → mip coordinates
const float u = (rx + cX) / scale;
const float v = (ry + cY) / scale;
const float mu = u * msx;
const float mv = v * msy;
float *const o = canvas + (size_t)4 * ((size_t)y * ow + x);
// antialiased rectangle coverage (~1px ramps at the source borders)
const float covu = CLAMP(mu + 0.5f, 0.0f, 1.0f) - CLAMP(mu - (fsw - 1.5f), 0.0f, 1.0f);
const float covv = CLAMP(mv + 0.5f, 0.0f, 1.0f) - CLAMP(mv - (fsh - 1.5f), 0.0f, 1.0f);
const float cov = covu * covv;
if(cov <= 0.0f)
{
o[0] = o[1] = o[2] = o[3] = 0.0f;
continue;
}
float px[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
dt_interpolation_compute_pixel4c(itor, src, px, mu, mv, (int)sw, (int)sh, src_stride);
o[0] = px[0];
o[1] = px[1];
o[2] = px[2];
o[3] = cov;
}
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
dt_free_align(mip);
// Scratch overlay was local to this call; free it now that the resample is done.
if(!use_cache) dt_free_align(cbuf);
return canvas;
}
/* Legacy (8-bit) compositor: the original behaviour, kept so edits made before
* the float path existed render identically. The overlay is rendered to an
* 8-bit Cairo ARGB32 surface (display-encoded sRGB, BGRA), scaled/rotated by
* Cairo, and returned as a Cairo ARGB32 buffer (row pitch = *out_stride bytes)
* that the caller must g_free(), or NULL on failure. Placement geometry is the
* same _overlay_compute_geometry() used by the float path.
*/
static guint8 *_get_overlay_argb(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out,
int *out_stride)
{
dt_iop_overlay_data_t *data = piece->data;
dt_iop_overlay_global_data_t *gd = self->global_data;
const int index = self->multi_priority;
const float angle = deg2radf(-data->rotate);
// ── Acquire / refresh the overlay buffer ─────────────────────────────────
dt_pthread_mutex_lock(&gd->overlay_threadsafe);
const gboolean use_cache = (self->dev->image_storage.id == darktable.develop->image_storage.id);
void *cbuf = NULL;
size_t cwidth = 0;
size_t cheight = 0;
void **pbuf = use_cache ? &gd->cache[index] : &cbuf;
size_t *pwidth = use_cache ? &gd->cwidth[index] : &cwidth;
size_t *pheight = use_cache ? &gd->cheight[index] : &cheight;
if(!dt_is_valid_imgid(data->imgid))
_clear_cache_entry(self, index);
// drop a cached buffer left over from the HQ float mode
if(use_cache && gd->cache[index] && !gd->cache_legacy[index])
_clear_cache_entry(self, index);
if(!*pbuf)
{
_setup_overlay(self, piece, TRUE /* legacy */, pbuf, pwidth, pheight);
if(use_cache)
gd->cache_legacy[index] = TRUE;
}
if(!*pbuf)
{
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
return NULL;
}
// ── Allocate the Cairo output canvas ─────────────────────────────────────
const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, roi_out->width);
if(stride < 0)
{
dt_print(DT_DEBUG_ALWAYS, "[overlay] cairo stride error");
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
return NULL;
}
guint8 *image = (guint8 *)g_try_malloc0_n(roi_out->height, stride);
if(!image)
{
dt_print(DT_DEBUG_ALWAYS, "[overlay] out of memory %d*%d", roi_out->height, stride);
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
return NULL;
}
cairo_surface_t *surface = cairo_image_surface_create_for_data(
image, CAIRO_FORMAT_ARGB32, roi_out->width, roi_out->height, stride);
if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS)
{
dt_print(DT_DEBUG_ALWAYS,
"[overlay] cairo surface error: %s",
cairo_status_to_string(cairo_surface_status(surface)));
cairo_surface_destroy(surface);
g_free(image);
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
return NULL;
}
// ── Cairo rendering ───────────────────────────────────────────────────────
// plugin_threadsafe guards Cairo/rsvg. Ordering: overlay_threadsafe first,
// then plugin_threadsafe — consistent everywhere, no deadlock risk.
dt_pthread_mutex_lock(&darktable.plugin_threadsafe);
const size_t bw = *pwidth;
const size_t bh = *pheight;
// Wrap *pbuf directly — no memcpy of the (potentially large) buffer.
cairo_surface_t *surface_two = dt_view_create_surface(*pbuf, bw, bh);
if(cairo_surface_status(surface_two) != CAIRO_STATUS_SUCCESS)
{
dt_print(DT_DEBUG_ALWAYS,
"[overlay] cairo overlay surface error: %s",
cairo_status_to_string(cairo_surface_status(surface_two)));
dt_pthread_mutex_unlock(&darktable.plugin_threadsafe);
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
cairo_surface_destroy(surface_two);
cairo_surface_destroy(surface);
if(!use_cache)
dt_free_align(cbuf);
g_free(image);
return NULL;
}
const _overlay_geometry_t geo = _overlay_compute_geometry(data, piece, roi_out, bw, bh, angle);
cairo_t *cr = cairo_create(surface);
cairo_translate(cr, -roi_in->x, -roi_in->y);
cairo_translate(cr, geo.tx * roi_out->scale, geo.ty * roi_out->scale);
cairo_translate(cr, geo.cX, geo.cY);
cairo_rotate(cr, angle);
cairo_translate(cr, -geo.cX, -geo.cY);
cairo_scale(cr, geo.scale, geo.scale);
cairo_surface_flush(surface_two);
cairo_set_source_surface(cr, surface_two, 0.0, 0.0);
cairo_paint(cr);
// cairo_paint() is synchronous for CPU surfaces: *pbuf is no longer read.
dt_pthread_mutex_unlock(&darktable.plugin_threadsafe);
dt_pthread_mutex_unlock(&gd->overlay_threadsafe);
cairo_destroy(cr);
cairo_surface_flush(surface);
cairo_surface_destroy(surface);
cairo_surface_destroy(surface_two); // drops reference to *pbuf, not ownership
// Scratch overlay was local to this call; free it now that Cairo is done.
if(!use_cache)
dt_free_align(cbuf);
*out_stride = stride;
return image;
}
void process(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const void *const ivoid,
void *const ovoid,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
const dt_iop_overlay_data_t *data = piece->data;
const int ch = piece->colors;
const float *const in = (const float *)ivoid;
float *const out = (float *)ovoid;
const float opacity = data->opacity / 100.0f;
if(data->compositing == DT_OVERLAY_COMPOSITE_LEGACY)
{
// legacy 8-bit Cairo ARGB32 overlay (display-encoded sRGB, BGRA byte order)
int stride = 0;
guint8 *image = _get_overlay_argb(self, piece, roi_in, roi_out, &stride);
if(!image)
{
dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, ch);
return;
}
DT_OMP_FOR(collapse(2))
for(int y = 0; y < roi_out->height; y++)
for(int x = 0; x < roi_out->width; x++)
{
const int j = y * roi_out->width + x;
const float *i = in + ch * j;
float *o = out + ch * j;
// Cairo ARGB32 (little-endian): byte order is [B, G, R, A]
const guint8 *s = image + (size_t)y * stride + (size_t)x * 4;
const float alpha = (s[3] / 255.0f) * opacity;
o[0] = (1.0f - alpha) * i[0] + opacity * s[2] / 255.0f;
o[1] = (1.0f - alpha) * i[1] + opacity * s[1] / 255.0f;
o[2] = (1.0f - alpha) * i[2] + opacity * s[0] / 255.0f;
o[3] = i[3];
}
g_free(image);
return;
}
// HQ: straight-alpha float RGBA overlay (R,G,B linear working RGB, A=coverage)
float *const image = _get_overlay_rgba_f(self, piece, roi_in, roi_out);
if(!image)
{
dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, ch);
return;
}
DT_OMP_FOR(collapse(2))
for(int y = 0; y < roi_out->height; y++)
for(int x = 0; x < roi_out->width; x++)
{
const int j = y * roi_out->width + x;
const float *i = in + ch * j;
float *o = out + ch * j;
const float *s = image + (size_t)4 * j;
const float alpha = s[3] * opacity;
o[0] = (1.0f - alpha) * i[0] + alpha * s[0];
o[1] = (1.0f - alpha) * i[1] + alpha * s[1];
o[2] = (1.0f - alpha) * i[2] + alpha * s[2];
o[3] = i[3];
}
dt_free_align(image);
}
#ifdef HAVE_OPENCL
// GPU alpha-blend path. The overlay resample runs on CPU (_get_overlay_rgba_f),
// but the pixel-blending of the full output image runs on the GPU.
int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
cl_mem dev_in, cl_mem dev_out,
const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
{
const dt_iop_overlay_data_t *data = piece->data;
const dt_iop_overlay_global_data_t *gd = self->global_data;
const int devid = piece->pipe->devid;
const int width = roi_out->width;
const int height = roi_out->height;
const float opacity = data->opacity / 100.0f;
if(data->compositing == DT_OVERLAY_COMPOSITE_LEGACY)
{
// legacy 8-bit Cairo ARGB32 overlay, blended by the legacy kernel (stride)
int stride = 0;
guint8 *limage = _get_overlay_argb(self, piece, roi_in, roi_out, &stride);
if(!limage)
return dt_opencl_enqueue_copy_image(
devid, dev_in, dev_out, CLIMG_ORIGIN, CLIMG_ORIGIN, (size_t[2]){ width, height });
cl_int lerr = DT_OPENCL_SYSMEM_ALLOCATION;
const size_t lsize = (size_t)height * stride;
cl_mem dev_lov = dt_opencl_alloc_device_buffer(devid, lsize);
if(!dev_lov)
goto legacy_cleanup;
lerr = dt_opencl_write_buffer_to_device(devid, limage, dev_lov, 0, lsize, TRUE);
if(lerr != CL_SUCCESS)
goto legacy_cleanup;
lerr = dt_opencl_enqueue_kernel_2d_args(devid,
gd->kernel_overlay_blend_legacy,
width,
height,
CLARG(dev_in),
CLARG(dev_lov),
CLARG(dev_out),
CLARG(width),
CLARG(height),
CLARG(opacity),
CLARG(stride));
legacy_cleanup:
dt_opencl_release_mem_object(dev_lov);
g_free(limage);
return lerr;
}
// HQ: straight-alpha float RGBA overlay (R,G,B linear working RGB, A=coverage)
float *image = _get_overlay_rgba_f(self, piece, roi_in, roi_out);
if(!image)
{
// No overlay: copy input to output on GPU
return dt_opencl_enqueue_copy_image(
devid, dev_in, dev_out, CLIMG_ORIGIN, CLIMG_ORIGIN, (size_t[2]){ width, height });
}
cl_int err = DT_OPENCL_SYSMEM_ALLOCATION;
// Upload the float RGBA overlay (4 floats/pixel) to GPU as a plain buffer
const size_t overlay_size = (size_t)width * height * 4 * sizeof(float);
cl_mem dev_overlay = dt_opencl_alloc_device_buffer(devid, overlay_size);
if(!dev_overlay) goto cleanup;
err = dt_opencl_write_buffer_to_device(devid, image, dev_overlay, 0, overlay_size, TRUE);
if(err != CL_SUCCESS) goto cleanup;
err = dt_opencl_enqueue_kernel_2d_args(devid,
gd->kernel_overlay_blend,
width,
height,
CLARG(dev_in),
CLARG(dev_overlay),
CLARG(dev_out),
CLARG(width),
CLARG(height),
CLARG(opacity));
cleanup:
dt_opencl_release_mem_object(dev_overlay);
dt_free_align(image);
return err;
}
#endif
static gboolean _draw_thumb(GtkWidget *area,
cairo_t *crf,
const dt_iop_module_t *self)
{
const dt_iop_overlay_gui_data_t *g = self->gui_data;
const dt_iop_overlay_params_t *p = self->params;
GtkAllocation allocation;
gtk_widget_get_allocation(area, &allocation);
const int width = allocation.width;
const int height = allocation.height;
if(dt_is_valid_imgid(p->imgid))
{
cairo_surface_t *surf = NULL;
const dt_view_surface_value_t res =
dt_view_image_get_surface(p->imgid, width, height, &surf, TRUE);
if(res == DT_VIEW_SURFACE_OK)
{
// compute dx/dy to center thumb on the area
const int img_width = cairo_image_surface_get_width(surf);
const int img_height = cairo_image_surface_get_height(surf);
int dx = 0;
int dy = 0;
if(img_width > img_height)
dy = (height - img_height) / 2;
else
dx = (width - img_width) / 2;
dt_gui_gtk_set_source_rgb(crf, DT_GUI_COLOR_THUMBNAIL_BG);
cairo_paint(crf);
cairo_set_source_surface(crf, surf, dx, dy);
cairo_paint(crf);
}
}
else
{
dt_gui_gtk_set_source_rgb(crf, DT_GUI_COLOR_BG);
cairo_set_line_width(crf, 3.0);
cairo_rectangle(crf, 0.0, 0.0, width, height);
if(g->drop_inside)
{
cairo_fill(crf);
}
cairo_move_to(crf, 0.0, 0.0);
cairo_line_to(crf, width, height);
cairo_move_to(crf, 0.0, height);
cairo_line_to(crf, width, 0.0);
cairo_stroke(crf);
PangoFontDescription *desc = dt_gui_get_font();
pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD);
pango_font_description_set_absolute_size(desc, DT_PIXEL_APPLY_DPI(12) * PANGO_SCALE);
PangoLayout *layout = pango_cairo_create_layout(crf);
pango_layout_set_font_description(layout, desc);
// TRANSLATORS: This text must be very narrow, check in the GUI that it is not truncated
pango_layout_set_text(layout, _("drop\nimage\nfrom filmstrip\nhere"), -1);
PangoRectangle ink;
pango_layout_get_pixel_extents(layout, &ink, NULL);
dt_gui_gtk_set_source_rgb(crf, DT_GUI_COLOR_LIGHTTABLE_FONT);
cairo_move_to(crf,
(width - ink.width) / 2.0,
(height - ink.height) / 2.0);
pango_cairo_show_layout(crf, layout);
pango_font_description_free(desc);
g_object_unref(layout);
}
return FALSE;
}
static void _alignment_callback(const GtkWidget *tb, dt_iop_module_t *self)
{
const dt_iop_overlay_gui_data_t *g = self->gui_data;
DT_GUARD_GUI_UPDATE();
dt_iop_overlay_params_t *p = self->params;
int index = -1;
for(int i = 0; i < 9; i++)
{
/* block signal handler */
g_signal_handlers_block_by_func(g->align[i], _alignment_callback, self);
if(GTK_WIDGET(g->align[i]) == tb)
{
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->align[i]), TRUE);
index = i;
}
else
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->align[i]), FALSE);
/* unblock signal handler */
g_signal_handlers_unblock_by_func(g->align[i], _alignment_callback, self);
}
p->alignment = index;
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
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)
{
if(old_version == 1)
{
// v1 had no compositing option: it always used the 8-bit Cairo path.
// Migrate to LEGACY so existing edits keep their original appearance.
typedef struct dt_iop_overlay_params_v1_t
{
float opacity;
float scale;
float xoffset;
float yoffset;
int alignment;
float rotate;
dt_iop_overlay_base_scale_t scale_base;
dt_iop_overlay_img_scale_t scale_img;
dt_iop_overlay_svg_scale_t scale_svg;
dt_imgid_t imgid;
char filename[1024];
size_t dummy0;
size_t dummy1;
int64_t dummy2;
} dt_iop_overlay_params_v1_t;
const dt_iop_overlay_params_v1_t *o = old_params;
dt_iop_overlay_params_t *n = calloc(1, sizeof(dt_iop_overlay_params_t));
n->opacity = o->opacity;
n->scale = o->scale;
n->xoffset = o->xoffset;
n->yoffset = o->yoffset;
n->alignment = o->alignment;
n->rotate = o->rotate;
n->scale_base = o->scale_base;
n->scale_img = o->scale_img;
n->scale_svg = o->scale_svg;
n->imgid = o->imgid;
dt_strlcpy_fixed_to_fixed(n->filename, sizeof(n->filename),
o->filename, sizeof(o->filename));
n->compositing = DT_OVERLAY_COMPOSITE_LEGACY;
n->dummy1 = 0;
n->dummy2 = 0;
*new_params = n;
*new_params_size = sizeof(dt_iop_overlay_params_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_overlay_params_t *p = (dt_iop_overlay_params_t *)p1;
dt_iop_overlay_data_t *d = piece->data;
d->opacity = p->opacity;
d->scale = p->scale;
d->rotate = p->rotate;
d->xoffset = p->xoffset;
d->yoffset = p->yoffset;
d->alignment = p->alignment;
d->scale_base = p->scale_base;
d->scale_img = p->scale_img;
d->scale_svg = p->scale_svg;
d->imgid = p->imgid;
d->compositing = p->compositing;
// stored params need not terminate the file name
dt_strlcpy_fixed_to_fixed(d->filename, sizeof(d->filename),
p->filename, sizeof(p->filename));
}
void init_pipe(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
piece->data = malloc(sizeof(dt_iop_overlay_data_t));
}
void cleanup_pipe(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
free(piece->data);
piece->data = NULL;
}
void gui_update(dt_iop_module_t *self)
{
const dt_iop_overlay_gui_data_t *g = self->gui_data;
dt_iop_overlay_params_t *p = self->params;
// the GUI handlers read the stored file name as a C string
p->filename[sizeof(p->filename) - 1] = '\0';
for(int i = 0; i < 9; i++)
{
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->align[i]), FALSE);
}
if(p->alignment >= 0 && p->alignment < 9)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->align[p->alignment]), TRUE);
if(p->scale_base == DT_SCALE_MAINMENU_ADVANCED)
{
gtk_widget_set_visible(GTK_WIDGET(g->scale_img), TRUE);
gtk_widget_set_visible(GTK_WIDGET(g->scale_svg), TRUE);
}
else
{
gtk_widget_set_visible(GTK_WIDGET(g->scale_img), FALSE);
gtk_widget_set_visible(GTK_WIDGET(g->scale_svg), FALSE);
}
gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
void reload_defaults(dt_iop_module_t *self)
{
dt_iop_overlay_params_t *p = self->params;
if(dt_is_valid_imgid(p->imgid))
dt_overlay_remove(self->dev->image_storage.id, p->imgid);
p->imgid = NO_IMGID;
}
void gui_reset(dt_iop_module_t *self)
{
const dt_iop_overlay_gui_data_t *g = self->gui_data;
dt_iop_overlay_params_t *p = self->params;
if(dt_is_valid_imgid(p->imgid))
dt_overlay_remove(self->dev->image_storage.id, p->imgid);
p->imgid = NO_IMGID;
gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
void gui_changed(dt_iop_module_t *self,
GtkWidget *w,
void *previous)
{
const dt_iop_overlay_gui_data_t *g = self->gui_data;
const dt_iop_overlay_params_t *p = self->params;
if(w == g->scale_base)
{
if(p->scale_base == DT_SCALE_MAINMENU_ADVANCED)
{
gtk_widget_set_visible(GTK_WIDGET(g->scale_img), TRUE);
gtk_widget_set_visible(GTK_WIDGET(g->scale_svg), TRUE);
}
else
{
gtk_widget_set_visible(GTK_WIDGET(g->scale_img), FALSE);
gtk_widget_set_visible(GTK_WIDGET(g->scale_svg), FALSE);
}
}
gtk_widget_queue_draw(GTK_WIDGET(g->area));
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
void init_global(dt_iop_module_so_t *self)
{
dt_iop_overlay_global_data_t *gd = calloc(1, sizeof(dt_iop_overlay_global_data_t));
dt_pthread_recursive_mutex_init(&gd->overlay_threadsafe);
#ifdef HAVE_OPENCL
const int program = 41; // overlay.cl
gd->kernel_overlay_blend = dt_opencl_create_kernel(program, "overlay_blend");
gd->kernel_overlay_blend_legacy = dt_opencl_create_kernel(program, "overlay_blend_legacy");
#else
gd->kernel_overlay_blend = -1;
gd->kernel_overlay_blend_legacy = -1;
#endif
self->data = gd;
}
void cleanup_global(dt_iop_module_so_t *self)
{
dt_iop_overlay_global_data_t *gd = self->data;
for(int k=0; kcache[k]);
dt_pthread_mutex_destroy(&gd->overlay_threadsafe);
#ifdef HAVE_OPENCL
dt_opencl_free_kernel(gd->kernel_overlay_blend);
dt_opencl_free_kernel(gd->kernel_overlay_blend_legacy);
#endif
free(gd);
self->data = NULL;
}
static void _signal_image_changed(gpointer instance, dt_iop_module_t *self)
{
if(!self) return;
for(int k=0; kmulti_priority);
dt_dev_reprocess_all(self->dev);
}
static void _drag_and_drop_received(GtkWidget *widget,
GdkDragContext *context,
gint x,
gint y,
const GtkSelectionData *selection_data,
const guint target_type,
const guint time,
dt_iop_module_t *self)
{
const dt_iop_overlay_gui_data_t *g = self->gui_data;
dt_iop_overlay_params_t *p = self->params;
gboolean success = FALSE;
if(selection_data != NULL && target_type == DND_TARGET_IMGID)
{
const int imgs_nb = gtk_selection_data_get_length(selection_data) / sizeof(dt_imgid_t);
if(imgs_nb)
{
const int index = self->multi_priority;
const dt_imgid_t *imgs = (dt_imgid_t *)gtk_selection_data_get_data(selection_data);
const dt_imgid_t imgid_intended_overlay = imgs[0];
const dt_imgid_t imgid_target_image = self->dev->image_storage.id;
// check for cross-references, that is this imgid_intended_overlay should not be using
// the current image as overlay.
if(dt_overlay_used_by(imgid_intended_overlay, imgid_target_image))
{
dt_control_log
(_("cannot use image %d as an overlay"
" as it is using the current image as an overlay, directly or indirectly"),
imgid_intended_overlay);
}
else
{
// remove previous overlay if valid
if(dt_is_valid_imgid(p->imgid))
dt_overlay_remove(imgid_target_image, p->imgid);
// and record the new one
p->imgid = imgid_intended_overlay;
_clear_cache_entry(self, index);
dt_overlay_record(imgid_target_image, imgid_intended_overlay);
// zero-fill so trailing bytes of any previous image path do not bleed
// into piece->hash (params are hashed by full buffer length)
memset(p->filename, 0, sizeof(p->filename));
dt_image_full_path(imgid_intended_overlay, p->filename, sizeof(p->filename), NULL);
dt_dev_add_history_item(darktable.develop, self, TRUE);
dt_control_queue_redraw_center();
gtk_widget_queue_draw(GTK_WIDGET(g->area));
success = TRUE;
}
}
}
gtk_drag_finish(context, success, FALSE, time);
}
static gboolean _on_drag_motion(GtkWidget *widget,
GdkDragContext *dc,
gint x,
gint y,
guint time,
const dt_iop_module_t *self)
{
dt_iop_overlay_gui_data_t *g = self->gui_data;
g->drop_inside = TRUE;
gtk_widget_queue_draw(widget);
return TRUE;
}
static void _on_drag_leave(GtkWidget *widget,
GdkDragContext *dc,
guint time,
const dt_iop_module_t *self)
{
dt_iop_overlay_gui_data_t *g = self->gui_data;
g->drop_inside = FALSE;
gtk_widget_queue_draw(widget);
}
void gui_init(dt_iop_module_t *self)
{
dt_iop_overlay_gui_data_t *g = IOP_GUI_ALLOC(overlay);
dt_iop_overlay_params_t *p = self->params;
GtkGrid *grid = GTK_GRID(gtk_grid_new());
gtk_grid_set_row_spacing(grid, DT_BAUHAUS_SPACE);
gtk_grid_set_column_spacing(grid, DT_PIXEL_APPLY_DPI(10));
int line = 0;
g->area = GTK_DRAWING_AREA(dtgtk_drawing_area_new_with_height(0));
g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(_draw_thumb), self);
gtk_widget_set_size_request(GTK_WIDGET(g->area), 150, 150);
gtk_grid_attach(grid, GTK_WIDGET(g->area), 0, line++, 1, 2);
gtk_widget_grab_focus(GTK_WIDGET(g->area));
gtk_drag_dest_set
(GTK_WIDGET(g->area), /* widget that will accept a drop */
GTK_DEST_DEFAULT_ALL, /* default actions for dest on DnD */
target_list_all, /* lists of target to support */
n_targets_all, /* size of list */
GDK_ACTION_MOVE /* what to do with data after dropped */
);
g_signal_connect(GTK_WIDGET(g->area),
"drag-data-received", G_CALLBACK(_drag_and_drop_received), self);
g_signal_connect(GTK_WIDGET(g->area),
"drag-motion", G_CALLBACK(_on_drag_motion), self);
g_signal_connect(GTK_WIDGET(g->area),
"drag-leave", G_CALLBACK(_on_drag_leave), self);
self->widget = dt_gui_vbox(grid);
// Add opacity/scale sliders to table
g->opacity = dt_bauhaus_slider_from_params(self, N_("opacity"));
dt_bauhaus_slider_set_format(g->opacity, "%");
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "placement")));
// rotate
g->rotate = dt_bauhaus_slider_from_params(self, "rotate");
dt_bauhaus_slider_set_format(g->rotate, "°");
dt_bauhaus_slider_set_factor(g->rotate, -1.f);
// scale
g->scale = dt_bauhaus_slider_from_params(self, N_("scale"));
dt_bauhaus_slider_set_soft_max(g->scale, 100.0);
dt_bauhaus_slider_set_format(g->scale, "%");
// legacy scale on drop-down
g->scale_base = dt_bauhaus_combobox_from_params(self, "scale_base");
gtk_widget_set_tooltip_text
(g->scale_base,
_("choose how to scale the overlay\n"
"• image: scale overlay relative to whole image\n"
"• larger border: scale larger overlay border relative to larger image border\n"
"• smaller border: scale larger overlay border relative to smaller image border\n"
"• height: scale overlay height to image height\n"
"• advanced options: choose overlay and image dimensions independently"));
// scale image reference
g->scale_img = dt_bauhaus_combobox_from_params(self, "scale_img");
gtk_widget_set_tooltip_text
(g->scale_img,
_("reference image dimension against which to scale the overlay"));
// scale marker reference
g->scale_svg = dt_bauhaus_combobox_from_params(self, "scale_svg");
gtk_widget_set_tooltip_text(g->scale_svg, _("overlay dimension to scale"));
// Create the 3x3 gtk table toggle button table...
GtkWidget *bat = gtk_grid_new();
GtkWidget *label = dtgtk_reset_label_new(_("alignment"),
self, &p->alignment, sizeof(p->alignment));
gtk_grid_attach(GTK_GRID(bat), label, 0, 0, 1, 3);
gtk_widget_set_hexpand(label, TRUE);
gtk_grid_set_row_spacing(GTK_GRID(bat), DT_PIXEL_APPLY_DPI(3));
gtk_grid_set_column_spacing(GTK_GRID(bat), DT_PIXEL_APPLY_DPI(3));
for(int i = 0; i < 9; i++)
{
g->align[i] = dtgtk_togglebutton_new(dtgtk_cairo_paint_alignment,
(CPF_SPECIAL_FLAG << i), NULL);
gtk_grid_attach(GTK_GRID(bat), GTK_WIDGET(g->align[i]), 1 + i%3, i/3, 1, 1);
g_signal_connect(G_OBJECT(g->align[i]), "toggled",
G_CALLBACK(_alignment_callback), self);
}
dt_gui_box_add(self->widget, bat);
// x/y offset
g->x_offset = dt_bauhaus_slider_from_params(self, "xoffset");
dt_bauhaus_slider_set_digits(g->x_offset, 3);
g->y_offset = dt_bauhaus_slider_from_params(self, "yoffset");
dt_bauhaus_slider_set_digits(g->y_offset, 3);
// Let's add some tooltips and hook up some signals...
gtk_widget_set_tooltip_text(g->opacity, _("the opacity of the overlay"));
gtk_widget_set_tooltip_text(g->scale, _("the scale of the overlay"));
gtk_widget_set_tooltip_text(g->rotate, _("the rotation of the overlay"));
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_MODULE_REMOVE, _module_remove_callback);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_IMAGE_CHANGED, _signal_image_changed);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_MODULE_MOVED, _signal_module_moved);
}
// 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