/* This file is part of darktable, Copyright (C) 2009-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/debug.h" #include "common/imagebuf.h" #include "common/interpolation.h" #include "common/math.h" #include "common/opencl.h" #include "control/conf.h" #include "control/control.h" #include "develop/develop.h" #include "develop/imageop.h" #include "develop/imageop_gui.h" #include "develop/tiling.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/guides.h" #include "gui/presets.h" #include "iop/iop_api.h" #include #include #include #include #include DT_MODULE_INTROSPECTION(5, dt_iop_clipping_params_t) /** flip H/V, rotate an image, then clip the buffer. */ typedef enum dt_iop_clipping_flags_t { FLAG_FLIP_HORIZONTAL = 1 << 0, FLAG_FLIP_VERTICAL = 1 << 1 } dt_iop_clipping_flags_t; typedef struct dt_iop_clipping_aspect_t { char *name; int d, n; } dt_iop_clipping_aspect_t; typedef struct dt_iop_clipping_params_t { float angle; // $MIN: -180.0 $MAX: 180.0 float cx; // $MIN: 0.0 $MAX: 1.0 $DESCRIPTION: "left" float cy; // $MIN: 0.0 $MAX: 1.0 $DESCRIPTION: "top" float cw; // $MIN: 0.0 $MAX: 1.0 $DESCRIPTION: "right" float ch; // $MIN: 0.0 $MAX: 1.0 $DESCRIPTION: "bottom" float k_h, k_v; float kxa; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.2 float kya; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.2 float kxb; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.8 float kyb; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.2 float kxc; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.8 float kyc; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.8 float kxd; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.2 float kyd; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.8 int k_type, k_sym; int k_apply; // $DEFAULT: 0 gboolean crop_auto; // $DEFAULT: TRUE $DESCRIPTION: "automatic cropping" int ratio_n; // $DEFAULT: -1 int ratio_d; // $DEFAULT: -1 } dt_iop_clipping_params_t; typedef enum _grab_region_t { GRAB_CENTER = 0, // 0 GRAB_LEFT = 1 << 0, // 1 GRAB_TOP = 1 << 1, // 2 GRAB_RIGHT = 1 << 2, // 4 GRAB_BOTTOM = 1 << 3, // 8 GRAB_TOP_LEFT = GRAB_TOP | GRAB_LEFT, // 3 GRAB_TOP_RIGHT = GRAB_TOP | GRAB_RIGHT, // 6 GRAB_BOTTOM_RIGHT = GRAB_BOTTOM | GRAB_RIGHT, // 12 GRAB_BOTTOM_LEFT = GRAB_BOTTOM | GRAB_LEFT, // 9 GRAB_HORIZONTAL = GRAB_LEFT | GRAB_RIGHT, // 5 GRAB_VERTICAL = GRAB_TOP | GRAB_BOTTOM, // 10 GRAB_ALL = GRAB_LEFT | GRAB_TOP | GRAB_RIGHT | GRAB_BOTTOM, // 15 GRAB_NONE = 1 << 4 // 16 } _grab_region_t; /* calculate the aspect ratios for current image */ static void keystone_type_populate(dt_iop_module_t *self, gboolean with_applied, int select); 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_clipping_params_v5_t { float angle; float cx; float cy; float cw; float ch; float k_h, k_v; float kxa; float kya; float kxb; float kyb; float kxc; float kyc; float kxd; float kyd; int k_type, k_sym; int k_apply; gboolean crop_auto; int ratio_n; int ratio_d; } dt_iop_clipping_params_v5_t; if(old_version == 2) { union { float f; uint32_t u; } k; // old structure def typedef struct old_params_t { float angle, cx, cy, cw, ch, k_h, k_v; } old_params_t; const old_params_t *o = (old_params_t *)old_params; dt_iop_clipping_params_v5_t *n = malloc(sizeof(dt_iop_clipping_params_v5_t)); k.f = o->k_h; int is_horizontal; if(k.u & 0x40000000u) is_horizontal = 1; else is_horizontal = 0; k.u &= ~0x40000000; if(is_horizontal) { n->k_h = k.f; n->k_v = 0.0f; } else { n->k_h = 0.0f; n->k_v = k.f; } n->angle = o->angle, n->cx = o->cx, n->cy = o->cy, n->cw = o->cw, n->ch = o->ch; n->kxa = n->kxd = 0.2f; n->kxc = n->kxb = 0.8f; n->kya = n->kyb = 0.2f; n->kyc = n->kyd = 0.8f; if(n->k_h == 0 && n->k_v == 0) n->k_type = 0; else n->k_type = 4; n->k_sym = 0; n->k_apply = 0; n->crop_auto = 1; // will be computed later, -2 here is used to detect uninitialized // value, -1 is already used for no clipping. n->ratio_d = n->ratio_n = -2; *new_params = n; *new_params_size = sizeof(dt_iop_clipping_params_v5_t); *new_version = 5; return 0; } if(old_version == 3) { // old structure def typedef struct old_params_t { float angle, cx, cy, cw, ch, k_h, k_v; } old_params_t; const old_params_t *o = (old_params_t *)old_params; dt_iop_clipping_params_v5_t *n = malloc(sizeof(dt_iop_clipping_params_v5_t)); n->angle = o->angle, n->cx = o->cx, n->cy = o->cy, n->cw = o->cw, n->ch = o->ch; n->k_h = o->k_h, n->k_v = o->k_v; n->kxa = n->kxd = 0.2f; n->kxc = n->kxb = 0.8f; n->kya = n->kyb = 0.2f; n->kyc = n->kyd = 0.8f; if(n->k_h == 0 && n->k_v == 0) n->k_type = 0; else n->k_type = 4; n->k_sym = 0; n->k_apply = 0; n->crop_auto = 1; // will be computed later, -2 here is used to detect uninitialized // value, -1 is already used for no clipping. n->ratio_d = n->ratio_n = -2; *new_params = n; *new_params_size = sizeof(dt_iop_clipping_params_v5_t); *new_version = 5; return 0; } if(old_version == 4) { typedef struct old_params_t { float angle, cx, cy, cw, ch, k_h, k_v; float kxa, kya, kxb, kyb, kxc, kyc, kxd, kyd; int k_type, k_sym; int k_apply, crop_auto; } old_params_t; const old_params_t *o = (old_params_t *)old_params; dt_iop_clipping_params_v5_t *n = malloc(sizeof(dt_iop_clipping_params_v5_t)); n->angle = o->angle, n->cx = o->cx, n->cy = o->cy, n->cw = o->cw, n->ch = o->ch; n->k_h = o->k_h, n->k_v = o->k_v; n->kxa = o->kxa, n->kxb = o->kxb, n->kxc = o->kxc, n->kxd = o->kxd; n->kya = o->kya, n->kyb = o->kyb, n->kyc = o->kyc, n->kyd = o->kyd; n->k_type = o->k_type; n->k_sym = o->k_sym; n->k_apply = o->k_apply; n->crop_auto = o->crop_auto; // will be computed later, -2 here is used to detect uninitialized // value, -1 is already used for no clipping. n->ratio_d = n->ratio_n = -2; *new_params = n; *new_params_size = sizeof(dt_iop_clipping_params_v5_t); *new_version = 5; return 0; } return 1; } typedef struct dt_iop_clipping_gui_data_t { GtkNotebook *notebook; GtkWidget *angle; GtkWidget *cx,*cy,*cw,*ch; GtkWidget *hvflip; GList *aspect_list; GtkWidget *aspect_presets; GtkWidget *keystone_type; GtkWidget *crop_auto; float button_down_zoom_x, button_down_zoom_y, button_down_angle; // position in image where the button has been pressed. /* current clip box */ float clip_x, clip_y, clip_w, clip_h, handle_x, handle_y; /* last box before change */ float prev_clip_x, prev_clip_y, prev_clip_w, prev_clip_h; /* maximum clip box */ float clip_max_x, clip_max_y, clip_max_w, clip_max_h; dt_hash_t clip_max_pipe_hash; int k_selected, k_show, k_selected_segment; gboolean k_drag; int cropping, straightening, applied; gboolean shift_hold; gboolean ctrl_hold; gboolean preview_ready; } dt_iop_clipping_gui_data_t; typedef struct dt_iop_clipping_data_t { float angle; // rotation angle float aspect; // forced aspect ratio float m[4]; // rot/mirror matrix float inv_m[4]; // inverse of m (m^-1) float ki_h, k_h; // keystone correction, ki and corrected k float ki_v, k_v; // keystone correction, ki and corrected k float tx, ty; // rotation center float cx, cy, cw, ch; // crop window float cix, ciy; // crop window on roi_out 1.0 scale uint32_t all_off; // 1: v and h off, else one of them is used uint32_t flags; // flipping flags uint32_t flip; // flipped output buffer so more area would fit. dt_boundingbox_t k_space; // space for the "destination" rectangle of the keystone quadrilateral float kxa, kya, kxb, kyb, kxc, kyc, kxd, kyd; // point of the "source" quadrilatere (modified if keystone is not "full") float a, b, d, e, g, h; // value of the transformation matrix (c=f=0 && i=1) int k_apply; int crop_auto; float enlarge_x, enlarge_y; } dt_iop_clipping_data_t; typedef struct dt_iop_clipping_global_data_t { int kernel_clip_rotate_bilinear; int kernel_clip_rotate_bicubic; int kernel_clip_rotate_lanczos2; int kernel_clip_rotate_lanczos3; } dt_iop_clipping_global_data_t; static void commit_box(dt_iop_module_t *self, dt_iop_clipping_gui_data_t *g, dt_iop_clipping_params_t *p); // helper to count corners in for loops: static inline void get_corner(const float *aabb, const int i, float *p) { for(int k = 0; k < 2; k++) p[k] = aabb[2 * ((i >> k) & 1) + k]; } static inline void adjust_aabb(const float *p, float *aabb) { aabb[0] = fminf(aabb[0], p[0]); aabb[1] = fminf(aabb[1], p[1]); aabb[2] = fmaxf(aabb[2], p[0]); aabb[3] = fmaxf(aabb[3], p[1]); } const char *deprecated_msg() { return _("this module is deprecated. please use the crop, orientation and/or rotate and perspective modules instead."); } const char *name() { return _("crop and rotate"); } const char *aliases() { return _("reframe|perspective|keystone|distortion"); } const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("change the framing and correct the perspective"), _("corrective or creative"), _("linear, RGB, scene-referred"), _("geometric, RGB"), _("linear, RGB, scene-referred")); } int default_group() { return IOP_GROUP_BASIC | IOP_GROUP_TECHNICAL; } int flags() { return IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_TILING_FULL_ROI | IOP_FLAGS_ONE_INSTANCE | IOP_FLAGS_ALLOW_FAST_PIPE | IOP_FLAGS_GUIDES_SPECIAL_DRAW | IOP_FLAGS_GUIDES_WIDGET | IOP_FLAGS_DEPRECATED; } int operation_tags() { return IOP_TAG_DISTORT | IOP_TAG_CROPPING; } int operation_tags_filter() { // switch off watermark, it gets confused. return IOP_TAG_DECORATION | IOP_TAG_CROPPING; } 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 void keystone_get_matrix(const dt_boundingbox_t k_space, float kxa, const float kxb, const float kxc, const float kxd, float kya, const float kyb, const float kyc, const float kyd, float *a, float *b, float *d, float *e, float *g, float *h) { *a = -((kxb * (kyd * kyd - kyc * kyd) - kxc * kyd * kyd + kyb * (kxc * kyd - kxd * kyd) + kxd * kyc * kyd) * k_space[2]) / (kxb * (kxc * kyd * kyd - kxd * kyc * kyd) + kyb * (kxd * kxd * kyc - kxc * kxd * kyd)); *b = ((kxb * (kxd * kyd - kxd * kyc) - kxc * kxd * kyd + kxd * kxd * kyc + (kxc * kxd - kxd * kxd) * kyb) * k_space[2]) / (kxb * (kxc * kyd * kyd - kxd * kyc * kyd) + kyb * (kxd * kxd * kyc - kxc * kxd * kyd)); *d = (kyb * (kxb * (kyd * k_space[3] - kyc * k_space[3]) - kxc * kyd * k_space[3] + kxd * kyc * k_space[3]) + kyb * kyb * (kxc * k_space[3] - kxd * k_space[3])) / (kxb * kyb * (-kxc * kyd - kxd * kyc) + kxb * kxb * kyc * kyd + kxc * kxd * kyb * kyb); *e = -(kxb * (kxd * kyc * k_space[3] - kxc * kyd * k_space[3]) + kxb * kxb * (kyd * k_space[3] - kyc * k_space[3]) + kxb * kyb * (kxc * k_space[3] - kxd * k_space[3])) / (kxb * kyb * (-kxc * kyd - kxd * kyc) + kxb * kxb * kyc * kyd + kxc * kxd * kyb * kyb); *g = -(kyb * (kxb * (2.0f * kxc * kyd * kyd - 2.0f * kxc * kyc * kyd) - kxc * kxc * kyd * kyd + 2.0f * kxc * kxd * kyc * kyd - kxd * kxd * kyc * kyc) + kxb * kxb * (kyc * kyc * kyd - kyc * kyd * kyd) + kyb * kyb * (-2.0f * kxc * kxd * kyd + kxc * kxc * kyd + kxd * kxd * kyc)) / (kxb * kxb * (kxd * kyc * kyc * kyd - kxc * kyc * kyd * kyd) + kxb * kyb * (kxc * kxc * kyd * kyd - kxd * kxd * kyc * kyc) + kyb * kyb * (kxc * kxd * kxd * kyc - kxc * kxc * kxd * kyd)); *h = (kxb * (-kxc * kxc * kyd * kyd + 2.0f * kxc * kxd * kyc * kyd - kxd * kxd * kyc * kyc) + kxb * kxb * (kxc * kyd * kyd - 2.0f * kxd * kyc * kyd + kxd * kyc * kyc) + kxb * (2.0f * kxd * kxd - 2.0f * kxc * kxd) * kyb * kyc + (kxc * kxc * kxd - kxc * kxd * kxd) * kyb * kyb) / (kxb * kxb * (kxd * kyc * kyc * kyd - kxc * kyc * kyd * kyd) + kxb * kyb * (kxc * kxc * kyd * kyd - kxd * kxd * kyc * kyc) + kyb * kyb * (kxc * kxd * kxd * kyc - kxc * kxc * kxd * kyd)); } DT_OMP_DECLARE_SIMD() static inline void keystone_backtransform(float *i, const dt_boundingbox_t k_space, const float a, const float b, const float d, const float e, const float g, const float h, const float kxa, const float kya) { const float xx = i[0] - k_space[0]; const float yy = i[1] - k_space[1]; const float div = ((d * xx - a * yy) * h + (b * yy - e * xx) * g + a * e - b * d); i[0] = (e * xx - b * yy) / div + kxa; i[1] = -(d * xx - a * yy) / div + kya; } DT_OMP_DECLARE_SIMD() static inline void keystone_transform(float *i, const dt_boundingbox_t k_space, const float a, const float b, const float d, const float e, const float g, const float h, const float kxa, const float kya) { const float xx = i[0] - kxa; const float yy = i[1] - kya; const float div = g * xx + h * yy + 1; i[0] = (a * xx + b * yy) / div + k_space[0]; i[1] = (d * xx + e * yy) / div + k_space[1]; } DT_OMP_DECLARE_SIMD() static inline void backtransform(float *x, float *o, const float *m, const float t_h, const float t_v) { x[1] /= (1.0f + x[0] * t_h); x[0] /= (1.0f + x[1] * t_v); mul_mat_vec_2(m, x, o); } DT_OMP_DECLARE_SIMD() static inline void inv_matrix(const float *m, float *inv_m) { const float det = (m[0] * m[3]) - (m[1] * m[2]); inv_m[0] = m[3] / det; inv_m[1] = -m[1] / det; inv_m[2] = -m[2] / det; inv_m[3] = m[0] / det; } DT_OMP_DECLARE_SIMD() static inline void transform(const float *x, float *o, const float *m, const float t_h, const float t_v) { mul_mat_vec_2(m, x, o); o[1] *= (1.0f + o[0] * t_h); o[0] *= (1.0f + o[1] * t_v); } gboolean distort_transform(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, float *const restrict points, size_t points_count) { // as dt_iop_roi_t contain int values and not floats, we can have some rounding errors // as a workaround, we use a factor for preview pipes float factor = 1.0f; if(dt_pipe_is_preview(piece->pipe)) factor = 100.0f; // we first need to be sure that all data values are computed // this is done in modify_roi_out fct, so we create tmp roi dt_iop_roi_t roi_out, roi_in; roi_in.width = piece->buf_in.width * factor; roi_in.height = piece->buf_in.height * factor; self->modify_roi_out(self, piece, &roi_out, &roi_in); const dt_iop_clipping_data_t *d = piece->data; const float rx = piece->buf_in.width; const float ry = piece->buf_in.height; const dt_boundingbox_t k_space = { d->k_space[0] * rx, d->k_space[1] * ry, d->k_space[2] * rx, d->k_space[3] * ry }; const float kxa = d->kxa * rx, kxb = d->kxb * rx, kxc = d->kxc * rx, kxd = d->kxd * rx; const float kya = d->kya * ry, kyb = d->kyb * ry, kyc = d->kyc * ry, kyd = d->kyd * ry; float ma = 0, mb = 0, md = 0, me = 0, mg = 0, mh = 0; if(d->k_apply == 1) keystone_get_matrix(k_space, kxa, kxb, kxc, kxd, kya, kyb, kyc, kyd, &ma, &mb, &md, &me, &mg, &mh); DT_OMP_FOR(if(points_count > 100)) for(size_t i = 0; i < points_count * 2; i += 2) { float pi[2], po[2]; pi[0] = points[i]; pi[1] = points[i + 1]; if(d->k_apply == 1) keystone_transform(pi, k_space, ma, mb, md, me, mg, mh, kxa, kya); pi[0] -= d->tx / factor; pi[1] -= d->ty / factor; // transform this point using matrix m transform(pi, po, d->inv_m, d->k_h, d->k_v); if(d->flip) { po[1] += d->tx / factor; po[0] += d->ty / factor; } else { po[0] += d->tx / factor; po[1] += d->ty / factor; } points[i] = po[0] - (d->cix - d->enlarge_x) / factor; points[i + 1] = po[1] - (d->ciy - d->enlarge_y) / factor; } // revert side-effects of the previous call to modify_roi_out // TODO: this is just a quick hack. we need a major revamp of this module! if(factor != 1.0f) { roi_in.width = piece->buf_in.width; roi_in.height = piece->buf_in.height; self->modify_roi_out(self, piece, &roi_out, &roi_in); } return TRUE; } gboolean distort_backtransform(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, float *const restrict points, size_t points_count) { // as dt_iop_roi_t contain int values and not floats, we can have some rounding errors // as a workaround, we use a factor for preview pipes float factor = 1.0f; if(dt_pipe_is_preview(piece->pipe)) factor = 100.0f; // we first need to be sure that all data values are computed // this is done in modify_roi_out fct, so we create tmp roi dt_iop_roi_t roi_out, roi_in; roi_in.width = piece->buf_in.width * factor; roi_in.height = piece->buf_in.height * factor; self->modify_roi_out(self, piece, &roi_out, &roi_in); const dt_iop_clipping_data_t *d = piece->data; const float rx = piece->buf_in.width; const float ry = piece->buf_in.height; const dt_boundingbox_t k_space = { d->k_space[0] * rx, d->k_space[1] * ry, d->k_space[2] * rx, d->k_space[3] * ry }; const float kxa = d->kxa * rx, kxb = d->kxb * rx, kxc = d->kxc * rx, kxd = d->kxd * rx; const float kya = d->kya * ry, kyb = d->kyb * ry, kyc = d->kyc * ry, kyd = d->kyd * ry; float ma, mb, md, me, mg, mh; if(d->k_apply == 1) keystone_get_matrix(k_space, kxa, kxb, kxc, kxd, kya, kyb, kyc, kyd, &ma, &mb, &md, &me, &mg, &mh); DT_OMP_FOR_SIMD(if(points_count > 100) aligned(points:64) aligned(k_space:16)) for(size_t i = 0; i < points_count * 2; i += 2) { float pi[2], po[2]; pi[0] = -(d->enlarge_x - d->cix) / factor + points[i]; pi[1] = -(d->enlarge_y - d->ciy) / factor + points[i + 1]; // transform this point using matrix m if(d->flip) { pi[1] -= d->tx / factor; pi[0] -= d->ty / factor; } else { pi[0] -= d->tx / factor; pi[1] -= d->ty / factor; } backtransform(pi, po, d->m, d->k_h, d->k_v); po[0] += d->tx / factor; po[1] += d->ty / factor; if(d->k_apply == 1) keystone_backtransform(po, k_space, ma, mb, md, me, mg, mh, kxa, kya); points[i] = po[0]; points[i + 1] = po[1]; } // revert side-effects of the previous call to modify_roi_out // TODO: this is just a quick hack. we need a major revamp of this module! if(factor != 1.0f) { roi_in.width = piece->buf_in.width; roi_in.height = piece->buf_in.height; self->modify_roi_out(self, piece, &roi_out, &roi_in); } return TRUE; } void distort_mask(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const float *const in, float *const out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { const dt_iop_clipping_data_t *d = piece->data; // only crop, no rot fast and sharp path: if(!d->flags && d->angle == 0.0 && d->all_off && roi_in->width == roi_out->width && roi_in->height == roi_out->height) { dt_iop_image_copy_by_size(out, in, roi_out->width, roi_out->height, 1); } else { const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); const float rx = piece->buf_in.width * roi_in->scale; const float ry = piece->buf_in.height * roi_in->scale; const dt_boundingbox_t k_space = { d->k_space[0] * rx, d->k_space[1] * ry, d->k_space[2] * rx, d->k_space[3] * ry }; const float kxa = d->kxa * rx, kxb = d->kxb * rx, kxc = d->kxc * rx, kxd = d->kxd * rx; const float kya = d->kya * ry, kyb = d->kyb * ry, kyc = d->kyc * ry, kyd = d->kyd * ry; float ma, mb, md, me, mg, mh; if(d->k_apply == 1) keystone_get_matrix(k_space, kxa, kxb, kxc, kxd, kya, kyb, kyc, kyd, &ma, &mb, &md, &me, &mg, &mh); DT_OMP_FOR(dt_omp_sharedconst(k_space)) // (slow) point-by-point transformation. // TODO: optimize with scanlines and linear steps between? for(int j = 0; j < roi_out->height; j++) { float *_out = out + (size_t)j * roi_out->width; for(int i = 0; i < roi_out->width; i++) { float pi[2] = { 0.0f }, po[2] = { 0.0f }; pi[0] = roi_out->x - roi_out->scale * d->enlarge_x + roi_out->scale * d->cix + i + 0.5f; pi[1] = roi_out->y - roi_out->scale * d->enlarge_y + roi_out->scale * d->ciy + j + 0.5f; // transform this point using matrix m if(d->flip) { pi[1] -= d->tx * roi_out->scale; pi[0] -= d->ty * roi_out->scale; } else { pi[0] -= d->tx * roi_out->scale; pi[1] -= d->ty * roi_out->scale; } pi[0] /= roi_out->scale; pi[1] /= roi_out->scale; backtransform(pi, po, d->m, d->k_h, d->k_v); po[0] *= roi_in->scale; po[1] *= roi_in->scale; po[0] += d->tx * roi_in->scale; po[1] += d->ty * roi_in->scale; if(d->k_apply == 1) keystone_backtransform(po, k_space, ma, mb, md, me, mg, mh, kxa, kya); po[0] -= roi_in->x + 0.5f; po[1] -= roi_in->y + 0.5f; _out[i] = CLIP(dt_interpolation_compute_sample(interpolation, in, po[0], po[1], roi_in->width, roi_in->height, 1, roi_in->width)); } } } } static int _iop_clipping_set_max_clip(dt_iop_module_t *self) { dt_iop_clipping_gui_data_t *g = self->gui_data; const dt_iop_clipping_params_t *p = self->params; if(g->clip_max_pipe_hash == self->dev->preview_pipe->backbuf_hash) return 1; // we want to know the size of the actual buffer dt_dev_pixelpipe_iop_t *piece = dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self); if(!piece) return 0; const float wp = piece->buf_out.width, hp = piece->buf_out.height; const float cx = CLAMPF(p->cx, 0.0f, 0.9f); const float cy = CLAMPF(p->cy, 0.0f, 0.9f); const float cw = CLAMPF(fabsf(p->cw), 0.1f, 1.0f); const float ch = CLAMPF(fabsf(p->ch), 0.1f, 1.0f); float points[8] = { 0.0f, 0.0f, wp, hp, cx * wp, cy * hp, cw * wp, ch * hp }; if(!dt_dev_distort_transform_plus(self->dev, self->dev->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, points, 4)) return 0; float wd, ht; dt_dev_get_preview_size(self->dev, &wd, &ht); g->clip_max_x = fmaxf(points[0] / wd, 0.0f); g->clip_max_y = fmaxf(points[1] / ht, 0.0f); g->clip_max_w = fminf((points[2] - points[0]) / wd, 1.0f); g->clip_max_h = fminf((points[3] - points[1]) / ht, 1.0f); // if clipping values are not null, this is undistorted values... g->clip_x = fmaxf(points[4] / wd, g->clip_max_x); g->clip_y = fmaxf(points[5] / ht, g->clip_max_y); g->clip_w = fminf((points[6] - points[4]) / wd, g->clip_max_w); g->clip_h = fminf((points[7] - points[5]) / ht, g->clip_max_h); g->clip_max_pipe_hash = self->dev->preview_pipe->backbuf_hash; return 1; } void modify_roi_out(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, dt_iop_roi_t *roi_out, const dt_iop_roi_t *roi_in_orig) { const dt_iop_roi_t roi_in_d = *roi_in_orig; const dt_iop_roi_t *roi_in = &roi_in_d; dt_iop_clipping_data_t *d = piece->data; // use whole-buffer roi information to create matrix and inverse. float rt[] = { cosf(d->angle), sinf(d->angle), -sinf(d->angle), cosf(d->angle) }; if(d->angle == 0.0f) { rt[0] = rt[3] = 1.0f; rt[1] = rt[2] = 0.0f; } for(int k = 0; k < 4; k++) d->m[k] = rt[k]; if(d->flags & FLAG_FLIP_HORIZONTAL) { d->m[0] = -rt[0]; d->m[2] = -rt[2]; } if(d->flags & FLAG_FLIP_VERTICAL) { d->m[1] = -rt[1]; d->m[3] = -rt[3]; } // now compute inverse of m inv_matrix(d->m, d->inv_m); if(d->k_apply == 0 && d->crop_auto == 1) // this is the old solution. { float inv_rt[4] = { 0.0f }; inv_matrix(rt, inv_rt); *roi_out = *roi_in; // correct keystone correction factors by resolution of this buffer const float kc = 1.0f / fminf(roi_in->width, roi_in->height); d->k_h = d->ki_h * kc; d->k_v = d->ki_v * kc; float cropscale = -1.0f; // check portrait/landscape orientation, whichever fits more area: const float oaabb[4] = { -.5f * roi_in->width, -.5f * roi_in->height, .5f * roi_in->width, .5f * roi_in->height }; for(int flip = 0; flip < 2; flip++) { const float roi_in_width = flip ? roi_in->height : roi_in->width; const float roi_in_height = flip ? roi_in->width : roi_in->height; float newcropscale = 1.0f; // fwd transform rotated points on corners and scale back inside roi_in bounds. float p[2], o[2]; const float aabb[4] = { -.5f * roi_in_width, -.5f * roi_in_height, .5f * roi_in_width, .5f * roi_in_height }; for(int c = 0; c < 4; c++) { get_corner(oaabb, c, p); transform(p, o, inv_rt, d->k_h, d->k_v); for(int k = 0; k < 2; k++) if(fabsf(o[k]) > 0.001f) newcropscale = fminf(newcropscale, aabb[(o[k] > 0 ? 2 : 0) + k] / o[k]); } if(newcropscale >= cropscale) { cropscale = newcropscale; // remember rotation center in whole-buffer coordinates: d->tx = roi_in->width * .5f; d->ty = roi_in->height * .5f; d->flip = flip; const float ach = d->ch - d->cy, acw = d->cw - d->cx; // rotate and clip to max extent if(flip) { roi_out->y = d->tx - (.5f - d->cy) * cropscale * roi_in->width; roi_out->x = d->ty - (.5f - d->cx) * cropscale * roi_in->height; roi_out->height = ach * cropscale * roi_in->width; roi_out->width = acw * cropscale * roi_in->height; } else { roi_out->x = d->tx - (.5f - d->cx) * cropscale * roi_in->width; roi_out->y = d->ty - (.5f - d->cy) * cropscale * roi_in->height; roi_out->width = acw * cropscale * roi_in->width; roi_out->height = ach * cropscale * roi_in->height; } } } } else { *roi_out = *roi_in; // set roi_out values with rotation and keystone // initial corners pos const dt_boundingbox_t corn_x = { 0.0f, roi_in->width, roi_in->width, 0.0f }; const dt_boundingbox_t corn_y = { 0.0f, 0.0f, roi_in->height, roi_in->height }; // destination corner points dt_boundingbox_t corn_out_x = { 0.0f }; dt_boundingbox_t corn_out_y = { 0.0f }; // we don't test image flip as autocrop is not completely ok... d->flip = 0; // we apply rotation and keystone to all those points float p[2], o[2]; for(int c = 0; c < 4; c++) { // keystone o[0] = corn_x[c]; o[1] = corn_y[c]; if(d->k_apply == 1) { o[0] /= (float)roi_in->width; o[1] /= (float)roi_in->height; keystone_transform(o, d->k_space, d->a, d->b, d->d, d->e, d->g, d->h, d->kxa, d->kya); o[0] *= roi_in->width; o[1] *= roi_in->height; } // rotation p[0] = o[0] - .5f * roi_in->width; p[1] = o[1] - .5f * roi_in->height; transform(p, o, d->inv_m, d->k_h, d->k_v); o[0] += .5f * roi_in->width; o[1] += .5f * roi_in->height; // and we set the values corn_out_x[c] = o[0]; corn_out_y[c] = o[1]; } float new_x = min4f(corn_out_x); if(new_x + roi_in->width < 0) new_x = -roi_in->width; float new_y = min4f(corn_out_y); if(new_y + roi_in->height < 0) new_y = -roi_in->height; float new_sc_x = max4f(corn_out_x); if(new_sc_x > 2.0f * roi_in->width) new_sc_x = 2.0f * roi_in->width; float new_sc_y = max4f(corn_out_y); if(new_sc_y > 2.0f * roi_in->height) new_sc_y = 2.0f * roi_in->height; // be careful, we don't want too small area here ! if(new_sc_x - new_x < roi_in->width / 8.0f) { float f = (new_sc_x + new_x) / 2.0f; if(f < roi_in->width / 16.0f) f = roi_in->width / 16.0f; if(f >= roi_in->width * 15.0f / 16.0f) f = roi_in->width * 15.0f / 16.0f - 1.0f; new_x = f - roi_in->width / 16.0f, new_sc_x = f + roi_in->width / 16.0f; } if(new_sc_y - new_y < roi_in->height / 8.0f) { float f = (new_sc_y + new_y) / 2.0f; if(f < roi_in->height / 16.0f) f = roi_in->height / 16.0f; if(f >= roi_in->height * 15.0f / 16.0f) f = roi_in->height * 15.0f / 16.0f - 1.0f; new_y = f - roi_in->height / 16.0f, new_sc_y = f + roi_in->height / 16.0f; } new_sc_y = new_sc_y - new_y; new_sc_x = new_sc_x - new_x; // now we apply the clipping new_x += d->cx * new_sc_x; new_y += d->cy * new_sc_y; new_sc_x *= d->cw - d->cx; new_sc_y *= d->ch - d->cy; d->enlarge_x = fmaxf(-new_x, 0.0f); roi_out->x = fmaxf(new_x, 0.0f); d->enlarge_y = fmaxf(-new_y, 0.0f); roi_out->y = fmaxf(new_y, 0.0f); roi_out->width = new_sc_x; roi_out->height = new_sc_y; d->tx = roi_in->width * .5f; d->ty = roi_in->height * .5f; } // sanity check. if(roi_out->x < 0) roi_out->x = 0; if(roi_out->y < 0) roi_out->y = 0; if(roi_out->width < 4 || roi_out->height < 4) { dt_print_pipe(DT_DEBUG_PIPE, "safety check", piece->pipe, self, DT_DEVICE_NONE, roi_in, roi_out); roi_out->x = roi_in->x; roi_out->y = roi_in->y; roi_out->width = roi_in->width; roi_out->height = roi_in->height; piece->enabled = FALSE; if(dt_pipe_is_full(piece->pipe)) dt_control_log (_("module '%s' has insane data so it is bypassed for now. you should disable it or change parameters\n"), self->name()); } // save rotation crop on output buffer in world scale: d->cix = roi_out->x; d->ciy = roi_out->y; } void modify_roi_in(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *roi_out, dt_iop_roi_t *roi_in) { const dt_iop_clipping_data_t *d = piece->data; *roi_in = *roi_out; // modify_roi_out took care of bounds checking for us. we hopefully do not get requests outside the clipping // area. // transform aabb back to roi_in // this aabb is set off by cx/cy const float so = roi_out->scale; const float kw = piece->buf_in.width * so, kh = piece->buf_in.height * so; const float roi_out_x = roi_out->x - d->enlarge_x * so, roi_out_y = roi_out->y - d->enlarge_y * so; float p[2], o[2]; const dt_boundingbox_t aabb = { roi_out_x + d->cix * so, roi_out_y + d->ciy * so, roi_out_x + d->cix * so + roi_out->width, roi_out_y + d->ciy * so + roi_out->height }; dt_boundingbox_t aabb_in = { FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX }; for(int c = 0; c < 4; c++) { // get corner points of roi_out get_corner(aabb, c, p); // backtransform aabb using m if(d->flip) { p[1] -= d->tx * so; p[0] -= d->ty * so; } else { p[0] -= d->tx * so; p[1] -= d->ty * so; } p[0] *= 1.0 / so; p[1] *= 1.0 / so; backtransform(p, o, d->m, d->k_h, d->k_v); o[0] *= so; o[1] *= so; o[0] += d->tx * so; o[1] += d->ty * so; o[0] /= kw; o[1] /= kh; if(d->k_apply == 1) keystone_backtransform(o, d->k_space, d->a, d->b, d->d, d->e, d->g, d->h, d->kxa, d->kya); o[0] *= kw; o[1] *= kh; // transform to roi_in space, get aabb. adjust_aabb(o, aabb_in); } // adjust roi_in to minimally needed region roi_in->x = aabb_in[0] - 1; roi_in->y = aabb_in[1] - 1; roi_in->width = aabb_in[2] - aabb_in[0] + 2; roi_in->height = aabb_in[3] - aabb_in[1] + 2; if(d->angle == 0.0f && d->all_off) { // just crop: make sure everything is precise. roi_in->x = aabb_in[0]; roi_in->y = aabb_in[1]; roi_in->width = roi_out->width; roi_in->height = roi_out->height; } // sanity check. const float scwidth = piece->buf_in.width * so, scheight = piece->buf_in.height * so; roi_in->x = CLAMP(roi_in->x, 0, (int)floorf(scwidth)); roi_in->y = CLAMP(roi_in->y, 0, (int)floorf(scheight)); roi_in->width = CLAMP(roi_in->width, 1, (int)ceilf(scwidth) - roi_in->x); roi_in->height = CLAMP(roi_in->height, 1, (int)ceilf(scheight) - roi_in->y); } // 3rd (final) pass: you get this input region (may be different from what was requested above), // do your best to fill the output region! 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) { if(!dt_iop_have_required_input_format(4/*need full-color pixels*/, self, piece->colors, ivoid, ovoid, roi_in, roi_out)) return; // unsupported format, image has been copied to output and module's trouble flag set const dt_iop_clipping_data_t *d = piece->data; const int ch = 4; const int ch_width = ch * roi_in->width; // only crop, no rot fast and sharp path: if(!d->flags && d->angle == 0.0 && d->all_off && roi_in->width == roi_out->width && roi_in->height == roi_out->height) { dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, ch); } else { const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); const float rx = piece->buf_in.width * roi_in->scale; const float ry = piece->buf_in.height * roi_in->scale; const dt_boundingbox_t k_space = { d->k_space[0] * rx, d->k_space[1] * ry, d->k_space[2] * rx, d->k_space[3] * ry }; const float kxa = d->kxa * rx, kxb = d->kxb * rx, kxc = d->kxc * rx, kxd = d->kxd * rx; const float kya = d->kya * ry, kyb = d->kyb * ry, kyc = d->kyc * ry, kyd = d->kyd * ry; float ma, mb, md, me, mg, mh; if(d->k_apply == 1) keystone_get_matrix(k_space, kxa, kxb, kxc, kxd, kya, kyb, kyc, kyd, &ma, &mb, &md, &me, &mg, &mh); DT_OMP_FOR(dt_omp_sharedconst(k_space)) // (slow) point-by-point transformation. // TODO: optimize with scanlines and linear steps between? for(int j = 0; j < roi_out->height; j++) { float *out = ((float *)ovoid) + (size_t)ch * j * roi_out->width; for(int i = 0; i < roi_out->width; i++) { float pi[2], po[2]; pi[0] = roi_out->x - roi_out->scale * d->enlarge_x + roi_out->scale * d->cix + i + 0.5f; pi[1] = roi_out->y - roi_out->scale * d->enlarge_y + roi_out->scale * d->ciy + j + 0.5f; // transform this point using matrix m if(d->flip) { pi[1] -= d->tx * roi_out->scale; pi[0] -= d->ty * roi_out->scale; } else { pi[0] -= d->tx * roi_out->scale; pi[1] -= d->ty * roi_out->scale; } pi[0] /= roi_out->scale; pi[1] /= roi_out->scale; backtransform(pi, po, d->m, d->k_h, d->k_v); po[0] *= roi_in->scale; po[1] *= roi_in->scale; po[0] += d->tx * roi_in->scale; po[1] += d->ty * roi_in->scale; if(d->k_apply == 1) keystone_backtransform(po, k_space, ma, mb, md, me, mg, mh, kxa, kya); po[0] -= roi_in->x + 0.5f; po[1] -= roi_in->y + 0.5f; dt_interpolation_compute_pixel4c(interpolation, (float *)ivoid, out + ch*i, po[0], po[1], roi_in->width, roi_in->height, ch_width); } } } } #ifdef HAVE_OPENCL 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_clipping_data_t *d = piece->data; const dt_iop_clipping_global_data_t *gd = self->global_data; cl_int err = DT_OPENCL_DEFAULT_ERROR; const int devid = piece->pipe->devid; const int width = roi_out->width; const int height = roi_out->height; // only crop, no rot fast and sharp path: if(!d->flags && d->angle == 0.0 && d->all_off && roi_in->width == roi_out->width && roi_in->height == roi_out->height) { const size_t region[2] = { width, height }; err = dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, CLIMG_ORIGIN, CLIMG_ORIGIN, region); if(err != CL_SUCCESS) goto error; } else { int crkernel = -1; const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); switch(interpolation->id) { case DT_INTERPOLATION_BILINEAR: crkernel = gd->kernel_clip_rotate_bilinear; break; case DT_INTERPOLATION_BICUBIC: crkernel = gd->kernel_clip_rotate_bicubic; break; case DT_INTERPOLATION_LANCZOS2: crkernel = gd->kernel_clip_rotate_lanczos2; break; case DT_INTERPOLATION_LANCZOS3: crkernel = gd->kernel_clip_rotate_lanczos3; break; default: return err; } const int roi[2] = { roi_in->x, roi_in->y }; const float roo[2] = { roi_out->x - roi_out->scale * d->enlarge_x + roi_out->scale * d->cix, roi_out->y - roi_out->scale * d->enlarge_y + roi_out->scale * d->ciy }; const float t[2] = { d->tx, d->ty }; const float k[2] = { d->k_h, d->k_v }; const float m[4] = { d->m[0], d->m[1], d->m[2], d->m[3] }; const float k_sizes[2] = { piece->buf_in.width * roi_in->scale, piece->buf_in.height * roi_in->scale }; const dt_boundingbox_t k_space = { d->k_space[0] * k_sizes[0], d->k_space[1] * k_sizes[1], d->k_apply ? d->k_space[2] * k_sizes[0] : 0.0f, d->k_space[3] * k_sizes[1] }; float ma, mb, md, me, mg, mh; keystone_get_matrix(k_space, d->kxa * k_sizes[0], d->kxb * k_sizes[0], d->kxc * k_sizes[0], d->kxd * k_sizes[0], d->kya * k_sizes[1], d->kyb * k_sizes[1], d->kyc * k_sizes[1], d->kyd * k_sizes[1], &ma, &mb, &md, &me, &mg, &mh); const float ka[2] = { d->kxa * k_sizes[0], d->kya * k_sizes[1] }; const float maa[4] = { ma, mb, md, me }; const float mbb[2] = { mg, mh }; err = dt_opencl_enqueue_kernel_2d_args(devid, crkernel, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(roi_in->width), CLARG(roi_in->height), CLARG(roi), CLARG(roo), CLARG(roi_in->scale), CLARG(roi_out->scale), CLARG(d->flip), CLARG(t), CLARG(k), CLARG(m), CLARG(k_space), CLARG(ka), CLARG(maa), CLARG(mbb)); } error: return err; } #endif void init_global(dt_iop_module_so_t *self) { const int program = 2; // basic.cl from programs.conf dt_iop_clipping_global_data_t *gd = malloc(sizeof(dt_iop_clipping_global_data_t)); self->data = gd; gd->kernel_clip_rotate_bilinear = dt_opencl_create_kernel(program, "clip_rotate_bilinear"); gd->kernel_clip_rotate_bicubic = dt_opencl_create_kernel(program, "clip_rotate_bicubic"); gd->kernel_clip_rotate_lanczos2 = dt_opencl_create_kernel(program, "clip_rotate_lanczos2"); gd->kernel_clip_rotate_lanczos3 = dt_opencl_create_kernel(program, "clip_rotate_lanczos3"); } void cleanup_global(dt_iop_module_so_t *self) { const dt_iop_clipping_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_clip_rotate_bilinear); dt_opencl_free_kernel(gd->kernel_clip_rotate_bicubic); dt_opencl_free_kernel(gd->kernel_clip_rotate_lanczos2); dt_opencl_free_kernel(gd->kernel_clip_rotate_lanczos3); free(self->data); self->data = NULL; } 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_clipping_params_t *p = (dt_iop_clipping_params_t *)p1; dt_iop_clipping_data_t *d = piece->data; // reset all values to be sure everything is initialized d->m[0] = d->m[3] = 1.0f; d->m[1] = d->m[2] = 0.0f; d->ki_h = d->ki_v = d->k_h = d->k_v = 0.0f; d->tx = d->ty = 0.0f; d->cix = d->ciy = 0.0f; d->kxa = d->kxd = d->kya = d->kyb = 0.0f; d->kxb = d->kxc = d->kyc = d->kyd = 0.6f; d->k_space[0] = d->k_space[1] = 0.2f; d->k_space[2] = d->k_space[3] = 0.6f; d->k_apply = 0; d->enlarge_x = d->enlarge_y = 0.0f; d->flip = 0; d->angle = deg2radf(p->angle); // image flip d->flags = (p->ch < 0 ? FLAG_FLIP_VERTICAL : 0) | (p->cw < 0 ? FLAG_FLIP_HORIZONTAL : 0); d->crop_auto = p->crop_auto; // keystones values computation if(p->k_type == 4) { // this is for old keystoning d->k_apply = 0; d->all_off = 1; if(fabsf(p->k_h) >= .0001) d->all_off = 0; if(p->k_h >= -1.0 && p->k_h <= 1.0) d->ki_h = p->k_h; else d->ki_h = 0.0f; if(fabsf(p->k_v) >= .0001) d->all_off = 0; if(p->k_v >= -1.0 && p->k_v <= 1.0) d->ki_v = p->k_v; else d->ki_v = 0.0f; } else if(p->k_type >= 0 && p->k_apply == 1) { // we reset old keystoning values d->ki_h = d->ki_v = 0; d->kxa = p->kxa; d->kxb = p->kxb; d->kxc = p->kxc; d->kxd = p->kxd; d->kya = p->kya; d->kyb = p->kyb; d->kyc = p->kyc; d->kyd = p->kyd; // we adjust the points if the keystoning is not in "full" mode if(p->k_type == 1) // we want horizontal points to be aligned { // line equations parameters const float a1 = (d->kxd - d->kxa) / (d->kyd - d->kya); const float b1 = d->kxa - a1 * d->kya; const float a2 = (d->kxc - d->kxb) / (d->kyc - d->kyb); const float b2 = d->kxb - a2 * d->kyb; if(d->kya > d->kyb) { // we move kya to the level of kyb d->kya = d->kyb; d->kxa = a1 * d->kya + b1; } else { // we move kyb to the level of kya d->kyb = d->kya; d->kxb = a2 * d->kyb + b2; } if(d->kyc > d->kyd) { // we move kyd to the level of kyc d->kyd = d->kyc; d->kxd = a1 * d->kyd + b1; } else { // we move kyc to the level of kyd d->kyc = d->kyd; d->kxc = a2 * d->kyc + b2; } } else if(p->k_type == 2) // we want vertical points to be aligned { // line equations parameters const float a1 = (d->kyb - d->kya) / (d->kxb - d->kxa); const float b1 = d->kya - a1 * d->kxa; const float a2 = (d->kyc - d->kyd) / (d->kxc - d->kxd); const float b2 = d->kyd - a2 * d->kxd; if(d->kxa > d->kxd) { // we move kxa to the level of kxd d->kxa = d->kxd; d->kya = a1 * d->kxa + b1; } else { // we move kyb to the level of kya d->kxd = d->kxa; d->kyd = a2 * d->kxd + b2; } if(d->kxc > d->kxb) { // we move kyd to the level of kyc d->kxb = d->kxc; d->kyb = a1 * d->kxb + b1; } else { // we move kyc to the level of kyd d->kxc = d->kxb; d->kyc = a2 * d->kxc + b2; } } d->k_space[0] = fabsf((d->kxa + d->kxd) / 2.0f); d->k_space[1] = fabsf((d->kya + d->kyb) / 2.0f); d->k_space[2] = fabsf((d->kxb + d->kxc) / 2.0f) - d->k_space[0]; d->k_space[3] = fabsf((d->kyc + d->kyd) / 2.0f) - d->k_space[1]; d->kxb = d->kxb - d->kxa; d->kxc = d->kxc - d->kxa; d->kxd = d->kxd - d->kxa; d->kyb = d->kyb - d->kya; d->kyc = d->kyc - d->kya; d->kyd = d->kyd - d->kya; keystone_get_matrix(d->k_space, d->kxa, d->kxb, d->kxc, d->kxd, d->kya, d->kyb, d->kyc, d->kyd, &d->a, &d->b, &d->d, &d->e, &d->g, &d->h); d->k_apply = 1; d->all_off = 0; d->crop_auto = 0; } else { d->all_off = 1; d->k_apply = 0; } if(dt_iop_has_focus(self)) { d->cx = 0.0f; d->cy = 0.0f; d->cw = 1.0f; d->ch = 1.0f; } else { d->cx = CLAMPF(p->cx, 0.0f, 0.9f); d->cy = CLAMPF(p->cy, 0.0f, 0.9f); d->cw = CLAMPF(fabsf(p->cw), 0.1f, 1.0f); d->ch = CLAMPF(fabsf(p->ch), 0.1f, 1.0f); // we show a error on stderr if we have clamped something if(d->cx != p->cx || d->cy != p->cy || d->cw != fabsf(p->cw) || d->ch != fabsf(p->ch)) { dt_print(DT_DEBUG_ALWAYS, "[crop&rotate] invalid crop data for %d : x=%0.04f y=%0.04f w=%0.04f h=%0.04f", pipe->image.id, p->cx, p->cy, p->cw, p->ch); } } } static void _event_preview_updated_callback(gpointer instance, dt_iop_module_t *self) { dt_iop_clipping_gui_data_t *g = self->gui_data; g->preview_ready = TRUE; DT_CONTROL_SIGNAL_DISCONNECT(_event_preview_updated_callback, self); // force max size to be recomputed g->clip_max_pipe_hash = DT_INVALID_HASH; } void gui_focus(dt_iop_module_t *self, gboolean in) { dt_iop_clipping_gui_data_t *g = self->gui_data; dt_iop_clipping_params_t *p = self->params; if(self->enabled) { if(in) { DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _event_preview_updated_callback); // got focus, grab stuff to gui: // need to get gui stuff for the first time for this image, g->clip_x = CLAMPF(p->cx, 0.0f, 0.9f); g->clip_y = CLAMPF(p->cy, 0.0f, 0.9f); g->clip_w = CLAMPF(fabsf(p->cw) - p->cx, 0.1f, 1.0f - g->clip_x); g->clip_h = CLAMPF(fabsf(p->ch) - p->cy, 0.1f, 1.0f - g->clip_y); } else { DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _event_preview_updated_callback); // lost focus, commit current params: // if the keystone setting is not finished, we discard it if(p->k_apply == 0 && p->k_type < 4 && p->k_type > 0) { keystone_type_populate(self, FALSE, 0); } // hack : commit_box use distort_transform routines with gui values to get params // but this values are accurate only if clipping is the gui_module... // so we temporary put back gui_module to clipping and revert once finished dt_iop_module_t *old_gui = self->dev->gui_module; self->dev->gui_module = self; commit_box(self, g, p); self->dev->gui_module = old_gui; g->clip_max_pipe_hash = DT_INVALID_HASH; } } else if(in) g->preview_ready = TRUE; } 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_clipping_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; } static float _ratio_get_aspect(dt_iop_module_t *self, GtkWidget *combo) { dt_iop_clipping_params_t *p = self->params; //retrieve full image dimensions to calculate aspect ratio if "original image" specified const char *text = dt_bauhaus_combobox_get_text(combo); if(text && !g_strcmp0(text,_("original image"))) { int proc_iwd = 0, proc_iht = 0; dt_dev_get_processed_size(&darktable.develop->full, &proc_iwd, &proc_iht); if(!(proc_iwd > 0 && proc_iht > 0)) return 0.0f; if((p->ratio_d > 0 && proc_iwd > proc_iht) || (p->ratio_d < 0 && proc_iwd < proc_iht)) return (float)proc_iwd / (float)proc_iht; else return (float)proc_iht / (float)proc_iwd; } // we want to know the size of the actual buffer dt_dev_pixelpipe_iop_t *piece = dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self); if(!piece) return 0.0f; const int iwd = piece->buf_in.width, iht = piece->buf_in.height; // if we do not have yet computed the aspect ratio, let's do it now if(p->ratio_d == -2 && p->ratio_n == -2) { if(fabsf(p->cw) == 1.0 && p->cx == 0.0 && fabsf(p->ch) == 1.0 && p->cy == 0.0) { p->ratio_d = -1; p->ratio_n = -1; } else { const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); const float whratio = ((float)(iwd - 2 * interpolation->width) * (fabsf(p->cw) - p->cx)) / ((float)(iht - 2 * interpolation->width) * (fabsf(p->ch) - p->cy)); const float ri = (float)iwd / (float)iht; const float prec = 0.0003f; if(fabsf(whratio - 3.0f / 2.0f) < prec) { p->ratio_d = 3; p->ratio_n = 2; } else if(fabsf(whratio - 2.0f / 1.0f) < prec) { p->ratio_d = 2; p->ratio_n = 1; } else if(fabsf(whratio - 7.0f / 5.0f) < prec) { p->ratio_d = 7; p->ratio_n = 5; } else if(fabsf(whratio - 4.0f / 3.0f) < prec) { p->ratio_d = 4; p->ratio_n = 3; } else if(fabsf(whratio - 5.0f / 4.0f) < prec) { p->ratio_d = 5; p->ratio_n = 4; } else if(fabsf(whratio - 1.0f / 1.0f) < prec) { p->ratio_d = 1; p->ratio_n = 1; } else if(fabsf(whratio - 16.0f / 9.0f) < prec) { p->ratio_d = 16; p->ratio_n = 9; } else if(fabsf(whratio - 16.0f / 10.0f) < prec) { p->ratio_d = 16; p->ratio_n = 10; } else if(fabsf(whratio - 244.5f / 203.2f) < prec) { p->ratio_d = 2445; p->ratio_n = 2032; } else if(fabsf(whratio - M_SQRT2_F) < prec) { p->ratio_d = 14142136; p->ratio_n = 10000000; } else if(fabsf(whratio - PHI) < prec) { p->ratio_d = 16180340; p->ratio_n = 10000000; } else if(fabsf(whratio - ri) < prec) { p->ratio_d = 1; p->ratio_n = 0; } else { p->ratio_d = 0; p->ratio_n = 0; } } } if(p->ratio_d == 0 && p->ratio_n == 0) return -1.0f; float d = 1.0f, n = 1.0f; if(p->ratio_n == 0) { d = copysignf(iwd, p->ratio_d); n = iht; } else { d = p->ratio_d; n = p->ratio_n; } // make aspect ratios like 3:2 and 2:3 to be the same thing const float dn = copysignf(MAX(fabsf(d), fabsf(n)), d); const float nn = copysignf(MIN(fabsf(d), fabsf(n)), n); if(dn < 0) return -nn / dn; else return dn / nn; } static void apply_box_aspect(dt_iop_module_t *self, const _grab_region_t grab) { dt_iop_clipping_gui_data_t *g = self->gui_data; int iwd, iht; dt_dev_get_processed_size(&darktable.develop->full, &iwd, &iht); // enforce aspect ratio. float aspect = _ratio_get_aspect(self, g->aspect_presets); // since one rarely changes between portrait and landscape by cropping, // long side of the crop box should match the long side of the image. if(iwd < iht) aspect = 1.0f / aspect; if(aspect > 0) { // if only one side changed, force aspect by two adjacent in equal parts // 1 2 4 8 : x y w h double clip_x = MAX(iwd * g->clip_x / (float)iwd, 0.0f); double clip_y = MAX(iht * g->clip_y / (float)iht, 0.0f); double clip_w = MIN(iwd * g->clip_w / (float)iwd, 1.0f); double clip_h = MIN(iht * g->clip_h / (float)iht, 1.0f); // if we only modified one dim, respectively, we wanted these values: const double target_h = (double)iwd * g->clip_w / ((double)iht * aspect); const double target_w = (double)iht * g->clip_h * aspect / (double)iwd; // i.e. target_w/h = w/target_h = aspect // first fix aspect ratio: // corners: move two adjacent if(grab == GRAB_TOP_LEFT) { // move x y clip_x = clip_x + clip_w - (target_w + clip_w) * .5; clip_y = clip_y + clip_h - (target_h + clip_h) * .5; clip_w = (target_w + clip_w) * .5; clip_h = (target_h + clip_h) * .5; } else if(grab == GRAB_TOP_RIGHT) // move y w { clip_y = clip_y + clip_h - (target_h + clip_h) * .5; clip_w = (target_w + clip_w) * .5; clip_h = (target_h + clip_h) * .5; } else if(grab == GRAB_BOTTOM_RIGHT) // move w h { clip_w = (target_w + clip_w) * .5; clip_h = (target_h + clip_h) * .5; } else if(grab == GRAB_BOTTOM_LEFT) // move h x { clip_h = (target_h + clip_h) * .5; clip_x = clip_x + clip_w - (target_w + clip_w) * .5; clip_w = (target_w + clip_w) * .5; } else if(grab & GRAB_HORIZONTAL) // dragged either x or w (1 4) { // change h and move y, h equally const double off = target_h - clip_h; clip_h = clip_h + off; clip_y = clip_y - .5 * off; } else if(grab & GRAB_VERTICAL) // dragged either y or h (2 8) { // change w and move x, w equally const double off = target_w - clip_w; clip_w = clip_w + off; clip_x = clip_x - .5 * off; } // now fix outside boxes: if(clip_x < g->clip_max_x) { const double prev_clip_h = clip_h; clip_h *= (clip_w + clip_x - g->clip_max_x) / clip_w; clip_w = clip_w + clip_x - g->clip_max_x; clip_x = g->clip_max_x; if(grab & GRAB_TOP) clip_y += prev_clip_h - clip_h; } if(clip_y < g->clip_max_y) { const double prev_clip_w = clip_w; clip_w *= (clip_h + clip_y - g->clip_max_y) / clip_h; clip_h = clip_h + clip_y - g->clip_max_y; clip_y = g->clip_max_y; if(grab & GRAB_LEFT) clip_x += prev_clip_w - clip_w; } if(clip_x + clip_w > g->clip_max_x + g->clip_max_w) { const double prev_clip_h = clip_h; clip_h *= (g->clip_max_x + g->clip_max_w - clip_x) / clip_w; clip_w = g->clip_max_x + g->clip_max_w - clip_x; if(grab & GRAB_TOP) clip_y += prev_clip_h - clip_h; } if(clip_y + clip_h > g->clip_max_y + g->clip_max_h) { const double prev_clip_w = clip_w; clip_w *= (g->clip_max_y + g->clip_max_h - clip_y) / clip_h; clip_h = g->clip_max_y + g->clip_max_h - clip_y; if(grab & GRAB_LEFT) clip_x += prev_clip_w - clip_w; } g->clip_x = fmaxf(clip_x, 0.0f); g->clip_y = fmaxf(clip_y, 0.0f); g->clip_w = fminf(clip_w, 1.0f); g->clip_h = fminf(clip_h, 1.0f); } } void reload_defaults(dt_iop_module_t *self) { const dt_image_t *img = &self->dev->image_storage; dt_iop_clipping_params_t *d = self->default_params; d->cx = img->usercrop[1]; d->cy = img->usercrop[0]; d->cw = img->usercrop[3]; d->ch = img->usercrop[2]; } static void _float_to_fract(const char *num, int *n, int *d) { char tnum[100]; gboolean sep_found = FALSE; const char *p = (char *)num; int k = 0; *d = 1; while(*p) { if(sep_found) *d *= 10; // look for decimal sep if(!sep_found && ((*p == ',') || (*p == '.'))) { sep_found = TRUE; } else if(*p < '0' || *p > '9') { *n = *d = 0; return; } else { tnum[k++] = *p; } p++; } tnum[k] = '\0'; *n = atoi(tnum); } static void aspect_presets_changed(GtkWidget *combo, dt_iop_module_t *self) { const dt_iop_clipping_gui_data_t *g = self->gui_data; dt_iop_clipping_params_t *p = self->params; const int which = dt_bauhaus_combobox_get(combo); int d = abs(p->ratio_d), n = p->ratio_n; const char *text = dt_bauhaus_combobox_get_text(combo); if(which < 0) { if(text) { const char *c = text; const char *end = text + strlen(text); while(*c != ':' && *c != '/' && c < end) c++; if(c < end - 1) { // input the exact fraction c++; const int dd = atoi(text); const int nn = atoi(c); // some sanity check if(nn == 0 || dd == 0) { dt_control_log(_("invalid ratio format. it should be \"number:number\"")); dt_bauhaus_combobox_set(combo, 0); return; } d = MAX(dd, nn); n = MIN(dd, nn); } else { // find the closest fraction from the input ratio int nn = 0, dd = 0; _float_to_fract(text, &nn, &dd); // some sanity check if(dd == 0 || nn == 0) { dt_control_log(_("invalid ratio format. it should be a positive number")); dt_bauhaus_combobox_set(combo, 0); return; } d = MAX(dd, nn); n = MIN(dd, nn); } // simplify the fraction with binary GCD - https://en.wikipedia.org/wiki/Greatest_common_divisor // search g and d such that g is odd and gcd(nn, dd) = g × 2^d int e = 0; int nn = abs(n); int dd = abs(d); while((nn % 2 == 0) && (dd % 2 == 0)) { nn /= 2; dd /= 2; e++; } while(nn != dd) { if(nn % 2 == 0) nn /= 2; else if(dd % 2 == 0) dd /= 2; else if(nn > dd) nn = (nn - dd) / 2; else dd = (dd - nn) / 2; } // reduce the fraction with the GCD n /= (nn * 1 << e); d /= (nn * 1 << e); } } else { d = n = 0; for(const GList *iter = g->aspect_list; iter; iter = g_list_next(iter)) { const dt_iop_clipping_aspect_t *aspect = iter->data; if(g_strcmp0(aspect->name, text) == 0) { d = aspect->d; n = aspect->n; break; } } } // now we save all that if it has changed if(d != abs(p->ratio_d) || n != p->ratio_n) { if(p->ratio_d >= 0) p->ratio_d = d; else p->ratio_d = -d; p->ratio_n = n; dt_conf_set_int("plugins/darkroom/clipping/ratio_d", abs(p->ratio_d)); dt_conf_set_int("plugins/darkroom/clipping/ratio_n", abs(p->ratio_n)); DT_GUARD_GUI_UPDATE(); apply_box_aspect(self, GRAB_HORIZONTAL); dt_control_queue_redraw_center(); } // Search if current aspect ratio matches something known int act = -1, i = 0; for(const GList *iter = g->aspect_list; iter; iter = g_list_next(iter)) { const dt_iop_clipping_aspect_t *aspect = iter->data; if((aspect->d == d) && (aspect->n == n)) { act = i; break; } i++; } // Update combobox label DT_ENTER_GUI_UPDATE(); if(act == -1) { // we got a custom ratio char str[128]; snprintf(str, sizeof(str), "%d:%d %2.2f", abs(p->ratio_d), abs(p->ratio_n), (float)abs(p->ratio_d) / (float)abs(p->ratio_n)); dt_bauhaus_combobox_set_text(g->aspect_presets, str); } else if(dt_bauhaus_combobox_get(g->aspect_presets) != act) // we got a default ratio dt_bauhaus_combobox_set(g->aspect_presets, act); DT_LEAVE_GUI_UPDATE(); } void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) { dt_iop_clipping_gui_data_t *g = self->gui_data; dt_iop_clipping_params_t *p = self->params; DT_ENTER_GUI_UPDATE(); if(w == g->cx) { dt_bauhaus_slider_set_soft_min(g->cw, p->cx + 0.10); g->clip_w = g->clip_x + g->clip_w - p->cx; g->clip_x = p->cx; } else if(w == g->cw) { dt_bauhaus_slider_set_soft_max(g->cx, p->cw - 0.10); g->clip_w = p->cw - g->clip_x; } else if(w == g->cy) { dt_bauhaus_slider_set_soft_min(g->ch, p->cy + 0.10); g->clip_h = g->clip_y + g->clip_h - p->cy; g->clip_y = p->cy; } else if(w == g->ch) { dt_bauhaus_slider_set_soft_max(g->cy, p->ch - 0.10); g->clip_h = p->ch - g->clip_y; } DT_LEAVE_GUI_UPDATE(); commit_box(self, g, p); if(w == g->crop_auto) dt_control_queue_redraw_center(); } void gui_reset(dt_iop_module_t *self) { dt_iop_clipping_gui_data_t *g = self->gui_data; /* reset aspect preset to default */ dt_conf_set_int("plugins/darkroom/clipping/ratio_d", 0); dt_conf_set_int("plugins/darkroom/clipping/ratio_n", 0); g->k_show = -1; } static void keystone_type_changed(GtkWidget *combo, dt_iop_module_t *self) { dt_iop_clipping_gui_data_t *g = self->gui_data; dt_iop_clipping_params_t *p = self->params; const int which = dt_bauhaus_combobox_get(combo); if((which == 5) || (which == 4 && p->k_h == 0 && p->k_v == 0)) { // if the keystone is applied,autocrop must be disabled ! gtk_widget_set_sensitive(g->crop_auto, FALSE); gtk_widget_set_sensitive(g->aspect_presets, TRUE); return; } // we recreate the list to be sure that the "already applied" entry is not display if(g->k_show == 2) { if(which == 0 || which == 4) g->k_show = 0; else g->k_show = 1; keystone_type_populate(self, FALSE, which); } // we set the params p->k_apply = 0; p->k_type = which; if(which == 0 || which == 4) g->k_show = 0; else g->k_show = 1; // we can enable autocrop gtk_widget_set_sensitive(g->crop_auto, (g->k_show == 0)); gtk_widget_set_sensitive(g->aspect_presets, (g->k_show == 0)); commit_box(self, g, p); dt_control_queue_redraw_center(); } static void keystone_type_populate(dt_iop_module_t *self, const gboolean with_applied, const int select) { const dt_iop_clipping_gui_data_t *g = self->gui_data; const dt_iop_clipping_params_t *p = self->params; dt_bauhaus_combobox_clear(g->keystone_type); dt_bauhaus_combobox_add(g->keystone_type, _("none")); dt_bauhaus_combobox_add(g->keystone_type, _("vertical")); dt_bauhaus_combobox_add(g->keystone_type, _("horizontal")); dt_bauhaus_combobox_add(g->keystone_type, _("full")); if(p->k_h != 0 || p->k_v != 0) dt_bauhaus_combobox_add(g->keystone_type, _("old system")); if(with_applied) dt_bauhaus_combobox_add(g->keystone_type, _("correction applied")); if(select < 0) return; int sel = 0; if(select > 10 && p->k_h == 0 && p->k_v == 0) sel = 4; else if(select > 10) sel = 5; else sel = select; dt_bauhaus_combobox_set(g->keystone_type, sel); // we have to be sure that the event is called... keystone_type_changed(g->keystone_type, self); } void gui_update(dt_iop_module_t *self) { dt_iop_clipping_gui_data_t *g = self->gui_data; dt_iop_clipping_params_t *p = self->params; int hvflip = 0; if(p->cw < 0) { if(p->ch < 0) hvflip = 3; // BOTH else hvflip = 1; // HORIZONTAL } else { if(p->ch < 0) hvflip = 2; // VERTICAL else hvflip = 0; // NONE } dt_bauhaus_combobox_set(g->hvflip, hvflip); // set aspect ratio based on the current image, if not found let's default // to free aspect. if(p->ratio_d == -2 && p->ratio_n == -2) _ratio_get_aspect(self, g->aspect_presets); if(p->ratio_d == -1 && p->ratio_n == -1) { p->ratio_d = dt_conf_get_int("plugins/darkroom/clipping/ratio_d"); p->ratio_n = dt_conf_get_int("plugins/darkroom/clipping/ratio_n"); } const int d = abs(p->ratio_d), n = p->ratio_n; int act = -1; int i = 0; for(const GList *iter = g->aspect_list; iter; iter = g_list_next(iter)) { const dt_iop_clipping_aspect_t *aspect = iter->data; if((aspect->d == d) && (aspect->n == n)) { act = i; break; } i++; } // keystone : if(p->k_apply == 1) g->k_show = 2; // needed to initialise correctly the combobox else g->k_show = -1; if(g->k_show == 2) { keystone_type_populate(self, TRUE, 99); } else if(g->k_show == -1) { keystone_type_populate(self, FALSE, p->k_type); } /* special handling the combobox when current act is already selected callback is not called, let do it our self then.. */ if(act == -1) { char str[128]; snprintf(str, sizeof(str), "%d:%d %2.2f", abs(p->ratio_d), abs(p->ratio_n), (float)abs(p->ratio_d) / (float)abs(p->ratio_n)); dt_bauhaus_combobox_set_text(g->aspect_presets, str); } if(dt_bauhaus_combobox_get(g->aspect_presets) == act) aspect_presets_changed(g->aspect_presets, self); else dt_bauhaus_combobox_set(g->aspect_presets, act); // reset gui draw box to what we have in the parameters: g->applied = 1; g->clip_x = CLAMPF(p->cx, 0.0f, 0.9f); g->clip_y = CLAMPF(p->cy, 0.0f, 0.9f); g->clip_w = CLAMPF(fabsf(p->cw) - p->cx, 0.1f, 1.0f - g->clip_x); g->clip_h = CLAMPF(fabsf(p->ch) - p->cy, 0.1f, 1.0f - g->clip_y); } static void hvflip_callback(GtkWidget *widget, dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_clipping_gui_data_t *g = self->gui_data; dt_iop_clipping_params_t *p = self->params; const int flip = dt_bauhaus_combobox_get(widget); p->cw = copysignf(p->cw, (flip & 1) ? -1.0 : 1.0); p->ch = copysignf(p->ch, (flip & 2) ? -1.0 : 1.0); commit_box(self, g, p); } static void aspect_flip(GtkWidget *button, dt_iop_module_t *self) { dt_iop_clipping_params_t *p = self->params; p->ratio_d = -p->ratio_d; apply_box_aspect(self, GRAB_HORIZONTAL); dt_control_queue_redraw_center(); } static gint _aspect_ratio_cmp(const dt_iop_clipping_aspect_t *a, const dt_iop_clipping_aspect_t *b) { // want most square at the end, and the most non-square at the beginning if((a->d == 0 || a->d == 1) && a->n == 0) return -1; const float ad = MAX(a->d, a->n); const float an = MIN(a->d, a->n); const float bd = MAX(b->d, b->n); const float bn = MIN(b->d, b->n); const float aratio = ad / an; const float bratio = bd / bn; if(aratio < bratio) return -1; const float prec = 0.0003f; if(fabsf(aratio - bratio) < prec) return 0; return 1; } static gchar *format_aspect(gchar *original, const int adim, const int bdim) { // Special ratios: freehand, original image if(bdim == 0) return g_strdup(original); return g_strdup_printf("%s %4.2f", original, (float)adim / (float)bdim); } void gui_init(dt_iop_module_t *self) { dt_iop_clipping_gui_data_t *g = IOP_GUI_ALLOC(clipping); g->aspect_list = NULL; g->clip_x = g->clip_y = g->handle_x = g->handle_y = 0.0; g->clip_w = g->clip_h = 1.0; g->clip_max_x = g->clip_max_y = 0.0; g->clip_max_w = g->clip_max_h = 1.0; g->clip_max_pipe_hash = DT_INVALID_HASH; g->cropping = 0; g->straightening = 0; g->applied = 1; g->shift_hold = FALSE; g->ctrl_hold = FALSE; g->k_drag = FALSE; g->k_show = -1; g->k_selected = -1; g->preview_ready = FALSE; static struct dt_action_def_t notebook_def = { }; g->notebook = dt_ui_notebook_new(¬ebook_def); dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_def); self->widget = dt_ui_notebook_page(g->notebook, N_("main"), NULL); g->hvflip = dt_bauhaus_combobox_new(self); dt_bauhaus_widget_set_label(g->hvflip, NULL, N_("flip")); dt_bauhaus_combobox_add(g->hvflip, _("none")); dt_bauhaus_combobox_add(g->hvflip, _("horizontal")); dt_bauhaus_combobox_add(g->hvflip, _("vertical")); dt_bauhaus_combobox_add(g->hvflip, _("both")); g_signal_connect(G_OBJECT(g->hvflip), "value-changed", G_CALLBACK(hvflip_callback), self); gtk_widget_set_tooltip_text(g->hvflip, _("mirror image horizontally and/or vertically")); dt_gui_box_add(self->widget, g->hvflip); g->angle = dt_bauhaus_slider_from_params(self, N_("angle")); dt_bauhaus_slider_set_factor(g->angle, -1.0); dt_bauhaus_slider_set_format(g->angle, "°"); gtk_widget_set_tooltip_text(g->angle, _("right-click and drag a line on the image to drag a straight line")); g->keystone_type = dt_bauhaus_combobox_new(self); dt_bauhaus_widget_set_label(g->keystone_type, NULL, N_("keystone")); dt_bauhaus_combobox_add(g->keystone_type, _("none")); dt_bauhaus_combobox_add(g->keystone_type, _("vertical")); dt_bauhaus_combobox_add(g->keystone_type, _("horizontal")); dt_bauhaus_combobox_add(g->keystone_type, _("full")); gtk_widget_set_tooltip_text(g->keystone_type, _("set perspective correction for your image")); g_signal_connect(G_OBJECT(g->keystone_type), "value-changed", G_CALLBACK(keystone_type_changed), self); dt_gui_box_add(self->widget, g->keystone_type); g->crop_auto = dt_bauhaus_combobox_from_params(self, "crop_auto"); gtk_widget_set_tooltip_text(g->crop_auto, _("automatically crop to avoid black edges")); const dt_iop_clipping_aspect_t aspects[] = { { _("freehand"), 0, 0 }, { _("original image"), 1, 0 }, { _("square"), 1, 1 }, { _("10:8 in print"), 2445, 2032 }, { _("5:4, 4x5, 8x10"), 5, 4 }, { _("11x14"), 14, 11 }, { _("8.5x11, letter"), 110, 85 }, { _("4:3, VGA, TV"), 4, 3 }, { _("5x7"), 7, 5 }, { _("ISO 216, DIN 476, A4"), 14142136, 10000000 }, { _("3:2, 4x6, 35mm"), 3, 2 }, { _("16:10, 8x5"), 16, 10 }, { _("golden cut"), 16180340, 10000000 }, { _("16:9, HDTV"), 16, 9 }, { _("widescreen"), 185, 100 }, { _("1.91:1"), 1080, 566 }, { _("2:1, univisium"), 2, 1 }, { _("cinemascope"), 235, 100 }, { _("21:9"), 237, 100 }, { _("anamorphic"), 239, 100 }, { _("3:1, panorama"), 300, 100 }, }; const int aspects_count = sizeof(aspects) / sizeof(dt_iop_clipping_aspect_t); for(int i = 0; i < aspects_count; i++) { dt_iop_clipping_aspect_t *aspect = g_malloc(sizeof(dt_iop_clipping_aspect_t)); aspect->name = format_aspect(aspects[i].name, aspects[i].d, aspects[i].n); aspect->d = aspects[i].d; aspect->n = aspects[i].n; g->aspect_list = g_list_append(g->aspect_list, aspect); } // add custom presets from config to the list GSList *custom_aspects = dt_conf_all_string_entries("plugins/darkroom/clipping/extra_aspect_ratios"); for(GSList *iter = custom_aspects; iter; iter = g_slist_next(iter)) { const dt_conf_string_entry_t *nv = iter->data; const char *c = nv->value; const char *end = nv->value + strlen(nv->value); while(*c != ':' && *c != '/' && c < end) c++; if(c < end - 1) { c++; const int d = atoi(nv->value); const int n = atoi(c); // some sanity check if(n == 0 || d == 0) { dt_print(DT_DEBUG_ALWAYS, "invalid ratio format for `%s'. it should be \"number:number\"", nv->key); dt_control_log(_("invalid ratio format for `%s'. it should be \"number:number\""), nv->key); continue; } dt_iop_clipping_aspect_t *aspect = g_malloc(sizeof(dt_iop_clipping_aspect_t)); aspect->name = format_aspect(nv->key, d, n); aspect->d = d; aspect->n = n; g->aspect_list = g_list_append(g->aspect_list, aspect); } else { dt_print(DT_DEBUG_ALWAYS, "invalid ratio format for `%s'. it should be \"number:number\"", nv->key); dt_control_log(_("invalid ratio format for `%s'. it should be \"number:number\""), nv->key); continue; } } g_slist_free_full(custom_aspects, dt_conf_string_entry_free); g->aspect_list = g_list_sort(g->aspect_list, (GCompareFunc)_aspect_ratio_cmp); // remove duplicates from the aspect ratio list int d = ((dt_iop_clipping_aspect_t *)g->aspect_list->data)->d + 1, n = ((dt_iop_clipping_aspect_t *)g->aspect_list->data)->n + 1; for(GList *iter = g->aspect_list; iter; iter = g_list_next(iter)) { const dt_iop_clipping_aspect_t *aspect = iter->data; const int dd = MIN(aspect->d, aspect->n); const int nn = MAX(aspect->d, aspect->n); if(dd == d && nn == n) { // same as the last one, remove this entry g_free(aspect->name); GList *prev = g_list_previous(iter); g->aspect_list = g_list_delete_link(g->aspect_list, iter); // it should never be NULL as the 1st element can't be a duplicate, but better safe than sorry iter = prev ? prev : g->aspect_list; } else { d = dd; n = nn; } } g->aspect_presets = dt_bauhaus_combobox_new(self); dt_bauhaus_combobox_set_editable(g->aspect_presets, 1); dt_bauhaus_widget_set_label(g->aspect_presets, NULL, N_("aspect")); for(GList *iter = g->aspect_list; iter; iter = g_list_next(iter)) { const dt_iop_clipping_aspect_t *aspect = iter->data; dt_bauhaus_combobox_add(g->aspect_presets, aspect->name); } dt_bauhaus_combobox_set(g->aspect_presets, 0); g_signal_connect(G_OBJECT(g->aspect_presets), "value-changed", G_CALLBACK(aspect_presets_changed), self); gtk_widget_set_tooltip_text(g->aspect_presets, _("set the aspect ratio\n" "the list is sorted: from most square to least square\n" "to enter custom aspect ratio open the combobox and type ratio in x:y or decimal format")); dt_bauhaus_widget_set_quad_paint(g->aspect_presets, dtgtk_cairo_paint_aspectflip, 0, NULL); g_signal_connect(G_OBJECT(g->aspect_presets), "quad-pressed", G_CALLBACK(aspect_flip), self); dt_gui_box_add(self->widget, g->aspect_presets); self->widget = dt_ui_notebook_page(g->notebook, _("margins"), NULL); g->cx = dt_bauhaus_slider_from_params(self, "cx"); dt_bauhaus_slider_set_digits(g->cx, 4); dt_bauhaus_slider_set_format(g->cx, "%"); gtk_widget_set_tooltip_text(g->cx, _("the left margin cannot overlap with the right margin")); g->cw = dt_bauhaus_slider_from_params(self, "cw"); dt_bauhaus_slider_set_digits(g->cw, 4); dt_bauhaus_slider_set_factor(g->cw, -100.0); dt_bauhaus_slider_set_offset(g->cw, 100.0); dt_bauhaus_slider_set_format(g->cw, "%"); gtk_widget_set_tooltip_text(g->cw, _("the right margin cannot overlap with the left margin")); g->cy = dt_bauhaus_slider_from_params(self, "cy"); dt_bauhaus_slider_set_digits(g->cy, 4); dt_bauhaus_slider_set_format(g->cy, "%"); gtk_widget_set_tooltip_text(g->cy, _("the top margin cannot overlap with the bottom margin")); g->ch = dt_bauhaus_slider_from_params(self, "ch"); dt_bauhaus_slider_set_digits(g->ch, 4); dt_bauhaus_slider_set_factor(g->ch, -100.0); dt_bauhaus_slider_set_offset(g->ch, 100.0); dt_bauhaus_slider_set_format(g->ch, "%"); gtk_widget_set_tooltip_text(g->ch, _("the bottom margin cannot overlap with the top margin")); self->widget = GTK_WIDGET(g->notebook); } static void free_aspect(const gpointer data) { dt_iop_clipping_aspect_t *aspect = (dt_iop_clipping_aspect_t *)data; g_free(aspect->name); aspect->name = NULL; g_free(aspect); } void gui_cleanup(dt_iop_module_t *self) { dt_iop_clipping_gui_data_t *g = self->gui_data; g_list_free_full(g->aspect_list, free_aspect); g->aspect_list = NULL; } static _grab_region_t get_grab(const float pzx, const float pzy, const dt_iop_clipping_gui_data_t *g, const float border, const float wd, const float ht) { _grab_region_t grab = GRAB_NONE; if(!(pzx < g->clip_x || pzx > g->clip_x + g->clip_w || pzy < g->clip_y || pzy > g->clip_y + g->clip_h)) { // we are inside the crop box grab = GRAB_CENTER; if(pzx >= g->clip_x && pzx * wd < g->clip_x * wd + border) grab |= GRAB_LEFT; // left border if(pzy >= g->clip_y && pzy * ht < g->clip_y * ht + border) grab |= GRAB_TOP; // top border if(pzx <= g->clip_x + g->clip_w && pzx * wd > (g->clip_w + g->clip_x) * wd - border) grab |= GRAB_RIGHT; // right border if(pzy <= g->clip_y + g->clip_h && pzy * ht > (g->clip_h + g->clip_y) * ht - border) grab |= GRAB_BOTTOM; // bottom border } return grab; } // draw symmetry signs static void gui_draw_sym(cairo_t *cr, const float x, const float y, const float scale, const gboolean active) { PangoLayout *layout; PangoRectangle ink; 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(16) * PANGO_SCALE * scale); layout = pango_cairo_create_layout(cr); pango_layout_set_font_description(layout, desc); pango_layout_set_text(layout, "ꝏ", -1); pango_layout_get_pixel_extents(layout, &ink, NULL); dt_draw_set_color_overlay(cr, TRUE, 0.5); dt_gui_draw_rounded_rectangle( cr, ink.width + DT_PIXEL_APPLY_DPI(4) * scale, ink.height + DT_PIXEL_APPLY_DPI(8) * scale, x - ink.width / 2.0f - DT_PIXEL_APPLY_DPI(2) * scale, y - ink.height / 2.0f - DT_PIXEL_APPLY_DPI(4) * scale); cairo_move_to(cr, x - ink.width / 2.0f, y - 3.0 * ink.height / 4.0f - DT_PIXEL_APPLY_DPI(4) * scale); if(active) cairo_set_source_rgba(cr, 1.0, 0.0, 0.0, .9); else cairo_set_source_rgba(cr, .2, .2, .2, .9); pango_cairo_show_layout(cr, layout); pango_font_description_free(desc); g_object_unref(layout); } // draw guides and handles over the image void gui_post_expose(dt_iop_module_t *self, cairo_t *cr, const float wd, const float ht, const float pzx, const float pzy, const float zoom_scale) { dt_develop_t *dev = self->dev; const dt_iop_clipping_gui_data_t *g = self->gui_data; const dt_iop_clipping_params_t *p = self->params; // we don't do anything if the image is not ready if(!g->preview_ready) return; // reapply box aspect to be sure that the ratio has not been modified by the keystone transform apply_box_aspect(self, GRAB_HORIZONTAL); const double dashes = DT_PIXEL_APPLY_DPI(5.0) / zoom_scale; // draw cropping window if(_iop_clipping_set_max_clip(self)) { cairo_set_source_rgba(cr, .2, .2, .2, .8); cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); cairo_rectangle(cr, g->clip_max_x * wd - 1.0, g->clip_max_y * ht - 1.0, g->clip_max_w * wd + 2.0, g->clip_max_h * ht + 2.0); cairo_rectangle(cr, g->clip_x * wd, g->clip_y * ht, g->clip_w * wd, g->clip_h * ht); cairo_fill(cr); } if(g->clip_x > .0f || g->clip_y > .0f || g->clip_w < 1.0f || g->clip_h < 1.0f) { cairo_set_line_width(cr, dashes / 2.0); cairo_rectangle(cr, g->clip_x * wd, g->clip_y * ht, g->clip_w * wd, g->clip_h * ht); dt_draw_set_color_overlay(cr, TRUE, 1.0); cairo_stroke(cr); } // draw cropping window dimensions if first mouse button is pressed if(darktable.control->button_down && darktable.control->button_down_which == 1 && g->k_show != 1) { char dimensions[16]; dimensions[0] = '\0'; PangoLayout *layout; PangoRectangle ext; 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(16) * PANGO_SCALE / zoom_scale); layout = pango_cairo_create_layout(cr); pango_layout_set_font_description(layout, desc); int procw, proch; dt_dev_get_processed_size(&dev->full, &procw, &proch); snprintf(dimensions, sizeof(dimensions), "%i x %i", (int)(procw * g->clip_w), (int)(proch * g->clip_h)); pango_layout_set_text(layout, dimensions, -1); pango_layout_get_pixel_extents(layout, NULL, &ext); const float text_w = ext.width; const float text_h = DT_PIXEL_APPLY_DPI(16+2) / zoom_scale; const float margin = DT_PIXEL_APPLY_DPI(6) / zoom_scale; float xp = (g->clip_x + g->clip_w * .5f) * wd - text_w * .5f; float yp = (g->clip_y + g->clip_h * .5f) * ht - text_h * .5f; // ensure that the rendered string remains visible within the window bounds double x1, y1, x2, y2; cairo_clip_extents(cr, &x1, &y1, &x2, &y2); xp = CLAMPF(xp, x1 + 2.0 * margin, x2 - text_w - 2.0 * margin); yp = CLAMPF(yp, y1 + 2.0 * margin, y2 - text_h - 2.0 * margin); cairo_set_source_rgba(cr, .5, .5, .5, .9); dt_gui_draw_rounded_rectangle(cr, text_w + 2 * margin, text_h + 2 * margin, xp - margin, yp - margin); cairo_set_source_rgb(cr, .7, .7, .7); cairo_move_to(cr, xp, yp); pango_cairo_show_layout(cr, layout); pango_font_description_free(desc); g_object_unref(layout); } // draw crop area guides dt_guides_draw(cr, g->clip_x * wd, g->clip_y * ht, g->clip_w * wd, g->clip_h * ht, zoom_scale); cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.0) / zoom_scale); dt_draw_set_color_overlay(cr, FALSE, 1.0); const int border = DT_PIXEL_APPLY_DPI(30.0) / zoom_scale; if(g->straightening) { PangoRectangle ink; PangoLayout *layout; 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(16) * PANGO_SCALE / zoom_scale); layout = pango_cairo_create_layout(cr); pango_layout_set_font_description(layout, desc); const float bzx = g->button_down_zoom_x, bzy = g->button_down_zoom_y; cairo_arc(cr, bzx * wd, bzy * ht, DT_PIXEL_APPLY_DPI(3), 0, 2.0 * M_PI); cairo_stroke(cr); cairo_arc(cr, pzx * wd, pzy * ht, DT_PIXEL_APPLY_DPI(3), 0, 2.0 * M_PI); cairo_stroke(cr); cairo_move_to(cr, bzx * wd, bzy * ht); cairo_line_to(cr, pzx * wd, pzy * ht); cairo_stroke(cr); // show rotation angle float dx = pzx * wd - bzx * wd, dy = pzy * ht - bzy * ht; if(dx < 0) { dx = -dx; dy = -dy; } float angle = rad2degf(atan2f(dy, dx)); if(angle > 45.0) angle -= 90; if(angle < -45.0) angle += 90; char view_angle[16]; view_angle[0] = '\0'; snprintf(view_angle, sizeof(view_angle), "%.2f°", angle); pango_layout_set_text(layout, view_angle, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); const float text_w = ink.width; const float text_h = DT_PIXEL_APPLY_DPI(16+2) / zoom_scale; const float margin = DT_PIXEL_APPLY_DPI(6) / zoom_scale; cairo_set_source_rgba(cr, .5, .5, .5, .9); const float xp = pzx * wd + DT_PIXEL_APPLY_DPI(20) / zoom_scale; const float yp = pzy * ht - ink.height; dt_gui_draw_rounded_rectangle (cr, text_w + 2 * margin, text_h + 2 * margin, xp - margin, yp - margin); cairo_set_source_rgba(cr, .7, .7, .7, .7); cairo_move_to(cr, xp, yp); pango_cairo_show_layout(cr, layout); pango_font_description_free(desc); g_object_unref(layout); } else if(g->k_show != 1) { const _grab_region_t grab = g->cropping ? g->cropping : get_grab(pzx, pzy, g, border, wd, ht); if(grab == GRAB_LEFT) cairo_rectangle(cr, g->clip_x * wd, g->clip_y * ht, border, g->clip_h * ht); if(grab == GRAB_TOP) cairo_rectangle(cr, g->clip_x * wd, g->clip_y * ht, g->clip_w * wd, border); if(grab == GRAB_TOP_LEFT) cairo_rectangle(cr, g->clip_x * wd, g->clip_y * ht, border, border); if(grab == GRAB_RIGHT) cairo_rectangle(cr, (g->clip_x + g->clip_w) * wd - border, g->clip_y * ht, border, g->clip_h * ht); if(grab == GRAB_BOTTOM) cairo_rectangle(cr, g->clip_x * wd, (g->clip_y + g->clip_h) * ht - border, g->clip_w * wd, border); if(grab == GRAB_BOTTOM_RIGHT) cairo_rectangle(cr, (g->clip_x + g->clip_w) * wd - border, (g->clip_y + g->clip_h) * ht - border, border, border); if(grab == GRAB_TOP_RIGHT) cairo_rectangle(cr, (g->clip_x + g->clip_w) * wd - border, g->clip_y * ht, border, border); if(grab == GRAB_BOTTOM_LEFT) cairo_rectangle(cr, g->clip_x * wd, (g->clip_y + g->clip_h) * ht - border, border, border); cairo_stroke(cr); } // draw keystone points and lines if(g->k_show == 1 && p->k_type > 0) { // points in screen space dt_dev_pixelpipe_iop_t *piece = dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self); if(!piece) return; const float wp = piece->buf_out.width, hp = piece->buf_out.height; float pts[8] = { p->kxa * wp, p->kya * hp, p->kxb * wp, p->kyb * hp, p->kxc * wp, p->kyc * hp, p->kxd * wp, p->kyd * hp }; if(dt_dev_distort_transform_plus(self->dev, self->dev->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 4)) { if(p->k_type == 3) { // determine extremity of the lines const float v1t = pts[0] - (pts[6] - pts[0]) * pts[1] / (pts[7] - pts[1]); const float v1b = (pts[6] - pts[0]) * ht / (pts[7] - pts[1]) + v1t; const float v2t = pts[2] - (pts[4] - pts[2]) * pts[3] / (pts[5] - pts[3]); const float v2b = (pts[4] - pts[2]) * ht / (pts[5] - pts[3]) + v2t; const float h1l = pts[1] - (pts[3] - pts[1]) * pts[0] / (pts[2] - pts[0]); const float h1r = (pts[3] - pts[1]) * wd / (pts[2] - pts[0]) + h1l; const float h2l = pts[7] - (pts[5] - pts[7]) * pts[6] / (pts[4] - pts[6]); const float h2r = (pts[5] - pts[7]) * wd / (pts[4] - pts[6]) + h2l; // draw the lines cairo_move_to(cr, v1t, 0); cairo_line_to(cr, v1b, ht); cairo_stroke(cr); cairo_move_to(cr, v2t, 0); cairo_line_to(cr, v2b, ht); cairo_stroke(cr); cairo_move_to(cr, 0, h1l); cairo_line_to(cr, wd, h1r); cairo_stroke(cr); cairo_move_to(cr, 0, h2l); cairo_line_to(cr, wd, h2r); cairo_stroke(cr); // redraw selected one cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0) / zoom_scale); if(g->k_selected_segment == 0) { cairo_move_to(cr, pts[0], pts[1]); cairo_line_to(cr, pts[2], pts[3]); cairo_stroke(cr); } else if(g->k_selected_segment == 1) { cairo_move_to(cr, pts[4], pts[5]); cairo_line_to(cr, pts[2], pts[3]); cairo_stroke(cr); } else if(g->k_selected_segment == 2) { cairo_move_to(cr, pts[4], pts[5]); cairo_line_to(cr, pts[6], pts[7]); cairo_stroke(cr); } else if(g->k_selected_segment == 3) { cairo_move_to(cr, pts[0], pts[1]); cairo_line_to(cr, pts[6], pts[7]); cairo_stroke(cr); } } else if(p->k_type == 2) { // determine extremity of the lines const float h1l = pts[1] - (pts[3] - pts[1]) * pts[0] / (pts[2] - pts[0]); const float h1r = (pts[3] - pts[1]) * wd / (pts[2] - pts[0]) + h1l; const float h2l = pts[7] - (pts[5] - pts[7]) * pts[6] / (pts[4] - pts[6]); const float h2r = (pts[5] - pts[7]) * wd / (pts[4] - pts[6]) + h2l; // draw the lines cairo_move_to(cr, 0, h1l); cairo_line_to(cr, wd, h1r); cairo_stroke(cr); cairo_move_to(cr, 0, h2l); cairo_line_to(cr, wd, h2r); cairo_stroke(cr); // redraw selected one cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0) / zoom_scale); if(g->k_selected_segment == 1) { cairo_move_to(cr, pts[4], pts[5]); cairo_line_to(cr, pts[2], pts[3]); cairo_stroke(cr); } else if(g->k_selected_segment == 3) { cairo_move_to(cr, pts[0], pts[1]); cairo_line_to(cr, pts[6], pts[7]); cairo_stroke(cr); } } else if(p->k_type == 1) { // determine extremity of the lines const float v1t = pts[0] - (pts[6] - pts[0]) * pts[1] / (pts[7] - pts[1]); const float v1b = (pts[6] - pts[0]) * ht / (pts[7] - pts[1]) + v1t; const float v2t = pts[2] - (pts[4] - pts[2]) * pts[3] / (pts[5] - pts[3]); const float v2b = (pts[4] - pts[2]) * ht / (pts[5] - pts[3]) + v2t; // draw the lines cairo_move_to(cr, v1t, 0); cairo_line_to(cr, v1b, ht); cairo_stroke(cr); cairo_move_to(cr, v2t, 0); cairo_line_to(cr, v2b, ht); cairo_stroke(cr); // redraw selected one cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0) / zoom_scale); if(g->k_selected_segment == 0) { cairo_move_to(cr, pts[0], pts[1]); cairo_line_to(cr, pts[2], pts[3]); cairo_stroke(cr); } else if(g->k_selected_segment == 2) { cairo_move_to(cr, pts[4], pts[5]); cairo_line_to(cr, pts[6], pts[7]); cairo_stroke(cr); } } // draw the points if(g->k_selected == 0) // point 1 { cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0) / zoom_scale); cairo_set_source_rgba(cr, 1.0, 0, 0, .8); } else { cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.0) / zoom_scale); cairo_set_source_rgba(cr, 1.0, 0, 0, .5); } cairo_arc(cr, pts[0], pts[1], DT_PIXEL_APPLY_DPI(5.0) / zoom_scale, 0, 2.0 * M_PI); cairo_stroke(cr); if(g->k_selected == 1) // point 2 { cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0) / zoom_scale); cairo_set_source_rgba(cr, 1.0, 0, 0, .8); } else { cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.0) / zoom_scale); cairo_set_source_rgba(cr, 1.0, 0, 0, .5); } cairo_arc(cr, pts[2], pts[3], DT_PIXEL_APPLY_DPI(5.0) / zoom_scale, 0, 2.0 * M_PI); cairo_stroke(cr); if(g->k_selected == 2) // point 3 { cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0) / zoom_scale); cairo_set_source_rgba(cr, 1.0, 0, 0, .8); } else { cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.0) / zoom_scale); cairo_set_source_rgba(cr, 1.0, 0, 0, .5); } cairo_arc(cr, pts[4], pts[5], DT_PIXEL_APPLY_DPI(5.0) / zoom_scale, 0, 2.0 * M_PI); cairo_stroke(cr); if(g->k_selected == 3) // point 4 { cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0) / zoom_scale); cairo_set_source_rgba(cr, 1.0, 0, 0, .8); } else { cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.0) / zoom_scale); cairo_set_source_rgba(cr, 1.0, 0, 0, .5); } cairo_arc(cr, pts[6], pts[7], DT_PIXEL_APPLY_DPI(5.0) / zoom_scale, 0, 2.0 * M_PI); cairo_stroke(cr); // draw the apply "button" PangoLayout *layout; PangoRectangle ink; 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(16) * PANGO_SCALE); layout = pango_cairo_create_layout(cr); pango_layout_set_font_description(layout, desc); cairo_set_font_size(cr, DT_PIXEL_APPLY_DPI(16)); pango_layout_set_text(layout, "ok", -1); pango_layout_get_pixel_extents(layout, &ink, NULL); const int c[2] = { (MIN(pts[4], pts[2]) + MAX(pts[0], pts[6])) / 2.0f, (MIN(pts[5], pts[7]) + MAX(pts[1], pts[3])) / 2.0f }; cairo_set_source_rgba(cr, .5, .5, .5, .9); dt_gui_draw_rounded_rectangle(cr, ink.width + DT_PIXEL_APPLY_DPI(8), ink.height + DT_PIXEL_APPLY_DPI(12), c[0] - ink.width / 2.0f - DT_PIXEL_APPLY_DPI(4), c[1] - ink.height / 2.0f - DT_PIXEL_APPLY_DPI(6)); cairo_move_to(cr, c[0] - ink.width / 2.0, c[1] - 3.0 * ink.height / 4.0); dt_draw_set_color_overlay(cr, FALSE, 0.9); pango_cairo_show_layout(cr, layout); pango_font_description_free(desc); g_object_unref(layout); // draw the symmetry buttons gboolean sym = FALSE; if(p->k_type == 1 || p->k_type == 3) { if(p->k_sym == 1 || p->k_sym == 3) sym = TRUE; gui_draw_sym(cr, (pts[0] + pts[6]) / 2.0f, (pts[1] + pts[7]) / 2.0f, 1.0f, sym); gui_draw_sym(cr, (pts[2] + pts[4]) / 2.0f, (pts[3] + pts[5]) / 2.0f, 1.0f, sym); } if(p->k_type == 2 || p->k_type == 3) { sym = (p->k_sym >= 2); gui_draw_sym(cr, (pts[0] + pts[2]) / 2.0f, (pts[1] + pts[3]) / 2.0f, 1.0f, sym); gui_draw_sym(cr, (pts[6] + pts[4]) / 2.0f, (pts[7] + pts[5]) / 2.0f, 1.0f, sym); } } } } // determine the distance between the segment [(xa,ya)(xb,yb)] and the point (xc,yc) static float dist_seg(const float xa, const float ya, const float xb, const float yb, const float xc, const float yc) { if(xa == xb && ya == yb) return (xc - xa) * (xc - xa) + (yc - ya) * (yc - ya); const float sx = xb - xa; const float sy = yb - ya; const float ux = xc - xa; const float uy = yc - ya; const float dp = sx * ux + sy * uy; if(dp < 0) return (xc - xa) * (xc - xa) + (yc - ya) * (yc - ya); const float sn2 = sx * sx + sy * sy; if(dp > sn2) return (xc - xb) * (xc - xb) + (yc - yb) * (yc - yb); const float ah2 = dp * dp / sn2; const float un2 = ux * ux + uy * uy; return un2 - ah2; } int mouse_moved(dt_iop_module_t *self, const float pzx, const float pzy, const double pressure, const int which, const float zoom_scale) { dt_iop_clipping_gui_data_t *g = self->gui_data; dt_iop_clipping_params_t *p = self->params; // we don't do anything if the image is not ready if(!g->preview_ready) return 0; float wd, ht; dt_dev_get_preview_size(self->dev, &wd, &ht); _iop_clipping_set_max_clip(self); _grab_region_t grab = get_grab(pzx, pzy, g, DT_PIXEL_APPLY_DPI(30.0) / zoom_scale, wd, ht); if(darktable.control->button_down && darktable.control->button_down_which == 3 && g->k_show != 1) { // second mouse button, straighten activated: g->straightening = 1; dt_control_change_cursor("crosshair"); dt_control_queue_redraw_center(); } else if(darktable.control->button_down && darktable.control->button_down_which == 1) { // case when we drag a point for keystone if(g->k_drag && g->k_selected >= 0) { float pts[2] = { pzx * wd, pzy * ht }; dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 1); const dt_dev_pixelpipe_iop_t *piece = dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self); const float xx = pts[0] / (float)piece->buf_out.width, yy = pts[1] / (float)piece->buf_out.height; if(g->k_selected == 0) { if(p->k_sym == 1 || p->k_sym == 3) p->kxa = fminf(xx, (p->kxc + p->kxd - 0.01f) / 2.0f), p->kxb = p->kxc - p->kxa + p->kxd; else p->kxa = fminf(xx, p->kxb - 0.01f); if(p->k_sym > 1) p->kya = fminf(yy, (p->kyc + p->kyb - 0.01f) / 2.0f), p->kyd = p->kyc - p->kya + p->kyb; else p->kya = fminf(yy, p->kyd - 0.01f); } else if(g->k_selected == 1) { if(p->k_sym == 1 || p->k_sym == 3) p->kxb = fmaxf(xx, (p->kxc + p->kxd + 0.01f) / 2.0f), p->kxa = p->kxc - p->kxb + p->kxd; else p->kxb = fmaxf(xx, p->kxa + 0.01f); if(p->k_sym > 1) p->kyb = fminf(yy, (p->kya + p->kyd - 0.01f) / 2.0f), p->kyc = p->kya - p->kyb + p->kyd; else p->kyb = fminf(yy, p->kyc - 0.01f); } else if(g->k_selected == 2) { if(p->k_sym == 1 || p->k_sym == 3) p->kxc = fmaxf(xx, (p->kxa + p->kxb + 0.01f) / 2.0f), p->kxd = p->kxa - p->kxc + p->kxb; else p->kxc = fmaxf(xx, p->kxd + 0.01f); if(p->k_sym > 1) p->kyc = fmaxf(yy, (p->kya + p->kyd + 0.01f) / 2.0f), p->kyb = p->kya - p->kyc + p->kyd; else p->kyc = fmaxf(yy, p->kyb + 0.01f); } else if(g->k_selected == 3) { if(p->k_sym == 1 || p->k_sym == 3) p->kxd = fminf(xx, (p->kxa + p->kxb - 0.01f) / 2.0f), p->kxc = p->kxa - p->kxd + p->kxb; else p->kxd = fminf(xx, p->kxc - 0.01f); if(p->k_sym > 1) p->kyd = fmaxf(yy, (p->kyc + p->kyb + 0.01f) / 2.0f), p->kya = p->kyc - p->kyd + p->kyb; else p->kyd = fmaxf(yy, p->kya + 0.01f); } dt_control_queue_redraw_center(); return 1; } // case when we drag a segment for keystone if(g->k_drag && g->k_selected_segment >= 0) { float decalx = pzx - g->button_down_zoom_x; float decaly = pzy - g->button_down_zoom_y; if(g->k_selected_segment == 0 && (p->k_type == 1 || p->k_type == 3)) { decaly = fminf(decaly, p->kyd - p->kya); decaly = fminf(decaly, p->kyc - p->kyb); p->kxa += decalx; p->kya += decaly; p->kxb += decalx; p->kyb += decaly; } else if(g->k_selected_segment == 1 && (p->k_type == 2 || p->k_type == 3)) { decalx = fmaxf(decalx, p->kxa - p->kxb); decalx = fmaxf(decalx, p->kxd - p->kxc); p->kxc += decalx; p->kyc += decaly; p->kxb += decalx; p->kyb += decaly; } else if(g->k_selected_segment == 2 && (p->k_type == 1 || p->k_type == 3)) { decaly = fmaxf(decaly, p->kya - p->kyd); decaly = fmaxf(decaly, p->kyb - p->kyc); p->kxc += decalx; p->kyc += decaly; p->kxd += decalx; p->kyd += decaly; } else if(g->k_selected_segment == 3 && (p->k_type == 2 || p->k_type == 3)) { decalx = fminf(decalx, p->kxb - p->kxa); decalx = fminf(decalx, p->kxc - p->kxd); p->kxa += decalx; p->kya += decaly; p->kxd += decalx; p->kyd += decaly; } g->button_down_zoom_x = pzx; g->button_down_zoom_y = pzy; dt_control_queue_redraw_center(); return 1; } // draw a light gray frame, to show it's not stored yet: g->applied = 0; // first mouse button, adjust cropping frame, but what do we do? const float bzx = g->button_down_zoom_x, bzy = g->button_down_zoom_y; if(g->cropping == GRAB_CENTER && !g->straightening && g->k_show != 1) { g->cropping = grab; if(grab == GRAB_CENTER) { g->cropping = GRAB_ALL; g->handle_x = g->clip_x; g->handle_y = g->clip_y; } if(grab & GRAB_LEFT) g->handle_x = bzx - g->clip_x; if(grab & GRAB_TOP) g->handle_y = bzy - g->clip_y; if(grab & GRAB_RIGHT) g->handle_x = bzx - (g->clip_w + g->clip_x); if(grab & GRAB_BOTTOM) g->handle_y = bzy - (g->clip_h + g->clip_y); if(!grab && darktable.control->button_down_which == 3) g->straightening = 1; } if(!g->straightening && darktable.control->button_down_which == GDK_BUTTON_PRIMARY && g->k_show != 1) { grab = g->cropping; if(grab == GRAB_ALL) { /* moving the crop window */ if(!g->shift_hold) g->clip_x = fminf(g->clip_max_w + g->clip_max_x - g->clip_w, fmaxf(g->clip_max_x, g->handle_x + pzx - bzx)); if(!g->ctrl_hold) g->clip_y = fminf(g->clip_max_h + g->clip_max_y - g->clip_h, fmaxf(g->clip_max_y, g->handle_y + pzy - bzy)); } else { /* changing the crop window */ if(g->shift_hold) { /* the center is locked, scale crop radial with locked ratio */ float xx = 0.0f; float yy = 0.0f; if(grab & GRAB_LEFT || grab & GRAB_RIGHT) xx = (grab & GRAB_LEFT) ? (pzx - bzx) : (bzx - pzx); if(grab & GRAB_TOP || grab & GRAB_BOTTOM) yy = (grab & GRAB_TOP) ? (pzy - bzy) : (bzy - pzy); float ratio = fmaxf((g->prev_clip_w - 2.0f * xx) / g->prev_clip_w, (g->prev_clip_h - 2.0f * yy) / g->prev_clip_h); // ensure we don't get too small crop size if(g->prev_clip_w * ratio < 0.1f) ratio = 0.1f / g->prev_clip_w; if(g->prev_clip_h * ratio < 0.1f) ratio = 0.1f / g->prev_clip_h; // ensure we don't have too big crop size if(g->prev_clip_w * ratio > g->clip_max_w) ratio = g->clip_max_w / g->prev_clip_w; if(g->prev_clip_h * ratio > g->clip_max_h) ratio = g->clip_max_h / g->prev_clip_h; // now that we are sure that the crop size is correct, we have to adjust top & left float nx = g->prev_clip_x - (g->prev_clip_w * ratio - g->prev_clip_w) / 2.0f; float ny = g->prev_clip_y - (g->prev_clip_h * ratio - g->prev_clip_h) / 2.0f; const float nw = g->prev_clip_w * ratio; const float nh = g->prev_clip_h * ratio; // move crop area to the right if needed nx = fmaxf(nx, g->clip_max_x); // move crop area to the left if needed nx = fminf(nx, g->clip_max_w + g->clip_max_x - nw); // move crop area to the bottom if needed ny = fmaxf(ny, g->clip_max_y); // move crop area to the top if needed ny = fminf(ny, g->clip_max_h + g->clip_max_y - nh); g->clip_x = nx; g->clip_y = ny; g->clip_w = nw; g->clip_h = nh; } else { if(grab & GRAB_LEFT) { const float old_clip_x = g->clip_x; g->clip_x = fminf(fmaxf(g->clip_max_x, pzx - g->handle_x), g->clip_x + g->clip_w - 0.1f); g->clip_w = old_clip_x + g->clip_w - g->clip_x; } if(grab & GRAB_TOP) { const float old_clip_y = g->clip_y; g->clip_y = fminf(fmaxf(g->clip_max_y, pzy - g->handle_y), g->clip_y + g->clip_h - 0.1f); g->clip_h = old_clip_y + g->clip_h - g->clip_y; } if(grab & GRAB_RIGHT) g->clip_w = fmaxf(0.1f, fminf(g->clip_max_w + g->clip_max_x, pzx - g->clip_x - g->handle_x)); if(grab & GRAB_BOTTOM) g->clip_h = fmaxf(0.1f, fminf(g->clip_max_h + g->clip_max_y, pzy - g->clip_y - g->handle_y)); } if(g->clip_x + g->clip_w > g->clip_max_w + g->clip_max_x) g->clip_w = g->clip_max_w + g->clip_max_x - g->clip_x; if(g->clip_y + g->clip_h > g->clip_max_h + g->clip_max_y) g->clip_h = g->clip_max_h + g->clip_max_y - g->clip_y; } apply_box_aspect(self, grab); // we save crop params too float points[4] = { g->clip_x * wd, g->clip_y * ht, (g->clip_x + g->clip_w) * wd, (g->clip_y + g->clip_h) * ht }; if(dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, points, 2)) { const dt_dev_pixelpipe_iop_t *piece = dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self); if(piece) { // only update the sliders, not the dt_iop_clipping_params_t structure, so that the call to // dt_control_queue_redraw_center below doesn't go rerun the pixelpipe because it thinks that // the image has changed when it actually hasn't, yet. The actual clipping parameters get set // from the sliders when the iop loses focus, at which time the final selected crop is applied. DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->cx, g->clip_x); dt_bauhaus_slider_set_soft_min(g->cw, g->clip_x + 0.10); dt_bauhaus_slider_set(g->cy, g->clip_y); dt_bauhaus_slider_set_soft_min(g->ch, g->clip_y + 0.10); dt_bauhaus_slider_set(g->cw, g->clip_x + g->clip_w); dt_bauhaus_slider_set_soft_max(g->cx, g->clip_x + g->clip_w - 0.10); dt_bauhaus_slider_set(g->ch, g->clip_y + g->clip_h); dt_bauhaus_slider_set_soft_max(g->cy, g->clip_y + g->clip_h - 0.10); DT_LEAVE_GUI_UPDATE(); } } } dt_control_queue_redraw_center(); return 1; } else if(grab && g->k_show != 1) { // hover over active borders, no button pressed // change mouse pointer if(grab == GRAB_LEFT) dt_control_change_cursor("w-resize"); else if(grab == GRAB_TOP) dt_control_change_cursor("n-resize"); else if(grab == GRAB_RIGHT) dt_control_change_cursor("e-resize"); else if(grab == GRAB_BOTTOM) dt_control_change_cursor("s-resize"); else if(grab == GRAB_TOP_LEFT) dt_control_change_cursor("nw-resize"); else if(grab == GRAB_TOP_RIGHT) dt_control_change_cursor("ne-resize"); else if(grab == GRAB_BOTTOM_RIGHT) dt_control_change_cursor("se-resize"); else if(grab == GRAB_BOTTOM_LEFT) dt_control_change_cursor("sw-resize"); else if(grab == GRAB_NONE) { dt_control_hinter_message(_("commit: double-click, straighten: right-drag")); dt_control_change_cursor("default"); } if(grab != GRAB_NONE) dt_control_hinter_message(_("resize: drag, keep aspect ratio: shift+drag\n" "straighten: right-drag")); dt_control_queue_redraw_center(); } else { // somewhere besides borders. maybe rotate? dt_control_change_cursor("move"); g->straightening = g->cropping = 0; // or maybe keystone const float ext = DT_PIXEL_APPLY_DPI(0.005f) / zoom_scale; if(g->k_show == 1 && g->k_drag == FALSE) { float pts[2] = { pzx * wd, pzy * ht }; dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 1); const dt_dev_pixelpipe_iop_t *piece = dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self); const float xx = pts[0] / (float)piece->buf_out.width, yy = pts[1] / (float)piece->buf_out.height; // are we near a keystone point ? g->k_selected = -1; g->k_selected_segment = -1; if(xx < p->kxa + ext && xx > p->kxa - ext && yy < p->kya + ext && yy > p->kya - ext) g->k_selected = 0; if(xx < p->kxb + ext && xx > p->kxb - ext && yy < p->kyb + ext && yy > p->kyb - ext) g->k_selected = 1; if(xx < p->kxc + ext && xx > p->kxc - ext && yy < p->kyc + ext && yy > p->kyc - ext) g->k_selected = 2; if(xx < p->kxd + ext && xx > p->kxd - ext && yy < p->kyd + ext && yy > p->kyd - ext) g->k_selected = 3; // or near a keystone segment if(g->k_selected < 0) { if(p->k_type == 1 || p->k_type == 3) { if(dist_seg(p->kxa, p->kya, p->kxb, p->kyb, xx, yy) < ext * ext) g->k_selected_segment = 0; else if(dist_seg(p->kxd, p->kyd, p->kxc, p->kyc, xx, yy) < ext * ext) g->k_selected_segment = 2; if(dist_seg(p->kxb, p->kyb, p->kxc, p->kyc, xx, yy) < ext * ext) g->k_selected_segment = 1; else if(dist_seg(p->kxd, p->kyd, p->kxa, p->kya, xx, yy) < ext * ext) g->k_selected_segment = 3; } } if(g->k_selected >= 0) { dt_control_hinter_message(_("move control point: drag")); dt_control_change_cursor("crosshair"); } else if(g->k_selected_segment >= 0) { dt_control_hinter_message(_("move line: drag, toggle symmetry: click ꝏ")); dt_control_change_cursor("crosshair"); } else { dt_control_hinter_message(_("apply: click ok, toggle symmetry: click ꝏ\n" "move line/control point: drag")); dt_control_change_cursor("move"); } } else { dt_control_hinter_message(_("move: drag, move vertically: shift+drag, move horizontally: ctrl+drag\n" "straighten: right-drag, commit: double-click")); } dt_control_queue_redraw_center(); } return 0; } static void commit_box(dt_iop_module_t *self, dt_iop_clipping_gui_data_t *g, dt_iop_clipping_params_t *p) { DT_GUARD_GUI_UPDATE(); g->cropping = 0; const dt_boundingbox_t old = { p->cx, p->cy, p->cw, p->ch }; const float eps = 1e-6f; // threshold to avoid rounding errors if(!self->enabled) { // first time crop, if any data is stored in p, it's obsolete: p->cx = p->cy = 0.0f; p->cw = p->ch = 1.0f; } // we want value in iop space float wd, ht; dt_dev_get_preview_size(self->dev, &wd, &ht); float points[4] = { g->clip_x * wd, g->clip_y * ht, (g->clip_x + g->clip_w) * wd, (g->clip_y + g->clip_h) * ht }; if(dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, points, 2)) { const dt_dev_pixelpipe_iop_t *piece = dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self); if(piece) { p->cx = CLAMPF(points[0] / (float)piece->buf_out.width, 0.0f, 0.9f); p->cy = CLAMPF(points[1] / (float)piece->buf_out.height, 0.0f, 0.9f); p->cw = copysignf(CLAMPF(points[2] / (float)piece->buf_out.width, 0.1f, 1.0f), p->cw); p->ch = copysignf(CLAMPF(points[3] / (float)piece->buf_out.height, 0.1f, 1.0f), p->ch); } } g->applied = 1; const gboolean changed = fabs(p->cx - old[0]) > eps || fabs(p->cy - old[1]) > eps || fabs(p->cw - old[2]) > eps || fabs(p->ch - old[3]) > eps; // dt_print(DT_DEBUG_ALWAYS, "[crop commit box] %i: %e %e %e %e", changed, p->cx - old[0], p->cy - old[1], p->cw - old[2], p->ch - old[3]); if(changed) dt_dev_add_history_item(darktable.develop, self, TRUE); } int button_released(dt_iop_module_t *self, const float pzx, const float pzy, const int which, const uint32_t state, const float zoom_scale) { dt_iop_clipping_gui_data_t *g = self->gui_data; // we don't do anything if the image is not ready if(!g->preview_ready) return 0; if(g->straightening) { // adjust the line with possible current angle and flip on this module dt_boundingbox_t pts = { pzx, pzy, g->button_down_zoom_x, g->button_down_zoom_y }; dt_dev_distort_backtransform_plus(self->dev, self->dev->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_INCL, pts, 2); float dx = pts[0] - pts[2]; float dy = pts[1] - pts[3]; if(dx < 0) { dx = -dx; dy = -dy; } float angle = atan2f(dy, dx); if(!(angle >= -M_PI_2f && angle <= M_PI_2f)) angle = 0.0f; float close = angle; if(close > M_PI_4f) close = M_PI_2f - close; else if(close < -M_PI_4f) close = -M_PI_2f - close; else close = -close; float a = rad2degf(close); if(a < -180.f) a += 360.f; if(a > 180.f) a -= 360.f; dt_bauhaus_slider_set(g->angle, a); dt_control_change_cursor("default"); } if(g->k_drag) g->k_drag = FALSE; /* reset internal ui states*/ g->straightening = g->cropping = 0; g->shift_hold = FALSE; g->ctrl_hold = FALSE; return 1; } int button_pressed(dt_iop_module_t *self, const float pzx, const float pzy, const double pressure, const int which, const int type, const uint32_t state, const float zoom_scale) { dt_iop_clipping_gui_data_t *g = self->gui_data; dt_iop_clipping_params_t *p = self->params; // we don't do anything if the image is not ready if(!g->preview_ready) return 0; // avoid unexpected back to lt mode: if(type == GDK_2BUTTON_PRESS && which == GDK_BUTTON_PRIMARY) { dt_iop_request_focus(NULL); commit_box(self, g, p); return 1; } if(which == GDK_BUTTON_SECONDARY || which == GDK_BUTTON_PRIMARY) { // switch module on already, other code depends in this: dt_dev_add_history_item(darktable.develop, self, TRUE); if(g->k_show == 1) { if(g->k_selected >= 0) g->k_drag = TRUE; // if a keystone point is selected then we start to drag it else // if we click to the apply button { const dt_dev_pixelpipe_iop_t *piece = dt_dev_distort_get_iop_pipe(self->dev, self->dev->preview_pipe, self); const float wp = piece->buf_out.width, hp = piece->buf_out.height; float pts[8] = { p->kxa * wp, p->kya * hp, p->kxb * wp, p->kyb * hp, p->kxc * wp, p->kyc * hp, p->kxd * wp, p->kyd * hp }; dt_dev_distort_transform_plus(self->dev, self->dev->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 4); float wd, ht; dt_dev_get_preview_size(self->dev, &wd, &ht); const float xx = pzx * wd, yy = pzy * ht; float c[2] = { (MIN(pts[4], pts[2]) + MAX(pts[0], pts[6])) / 2.0f, (MIN(pts[5], pts[7]) + MAX(pts[1], pts[3])) / 2.0f }; const float ext = DT_PIXEL_APPLY_DPI(10.0) / (zoom_scale); // Apply button if(xx > c[0] - ext && xx < c[0] + ext && yy > c[1] - ext && yy < c[1] + ext) { // add an entry to the combo box and select it keystone_type_populate(self, TRUE, 99); // reset gui settings g->k_show = 2; g->k_selected = -1; g->k_drag = FALSE; // do the changes p->k_apply = 1; commit_box(self, g, p); } else { // Horizontal symmetry button (1) c[0] = (pts[0] + pts[6]) / 2.0f, c[1] = (pts[1] + pts[7]) / 2.0f; if(xx > c[0] - ext && xx < c[0] + ext && yy > c[1] - ext && yy < c[1] + ext && (p->k_type == 1 || p->k_type == 3)) { if(p->k_sym == 0) p->k_sym = 1; else if(p->k_sym == 1) p->k_sym = 0; else if(p->k_sym == 2) p->k_sym = 3; else p->k_sym = 2; } else { // Horizontal symmetry button (2) c[0] = (pts[2] + pts[4]) / 2.0f, c[1] = (pts[3] + pts[5]) / 2.0f; if(xx > c[0] - ext && xx < c[0] + ext && yy > c[1] - ext && yy < c[1] + ext && (p->k_type == 1 || p->k_type == 3)) { if(p->k_sym == 0) p->k_sym = 1; else if(p->k_sym == 1) p->k_sym = 0; else if(p->k_sym == 2) p->k_sym = 3; else p->k_sym = 2; } else { // vertical symmetry button (1) c[0] = (pts[2] + pts[0]) / 2.0f, c[1] = (pts[3] + pts[1]) / 2.0f; if(xx > c[0] - ext && xx < c[0] + ext && yy > c[1] - ext && yy < c[1] + ext && (p->k_type == 2 || p->k_type == 3)) { if(p->k_sym == 0) p->k_sym = 2; else if(p->k_sym == 1) p->k_sym = 3; else if(p->k_sym == 2) p->k_sym = 0; else p->k_sym = 1; } else { // vertical symmetry button (2) c[0] = (pts[4] + pts[6]) / 2.0f, c[1] = (pts[5] + pts[7]) / 2.0f; if(xx > c[0] - ext && xx < c[0] + ext && yy > c[1] - ext && yy < c[1] + ext && (p->k_type == 2 || p->k_type == 3)) { if(p->k_sym == 0) p->k_sym = 2; else if(p->k_sym == 1) p->k_sym = 3; else if(p->k_sym == 2) p->k_sym = 0; else p->k_sym = 1; } else { // dragging a border ? if(g->k_selected_segment >= 0) { g->button_down_zoom_x = pzx; g->button_down_zoom_y = pzy; g->k_drag = TRUE; } } } } } } } } else { g->button_down_zoom_x = pzx; g->button_down_zoom_y = pzy; g->button_down_angle = p->angle; /* update prev clip box with current */ g->prev_clip_x = g->clip_x; g->prev_clip_y = g->clip_y; g->prev_clip_w = g->clip_w; g->prev_clip_h = g->clip_h; /* if shift is pressed, then lock crop on center */ if(dt_modifiers_include(state, GDK_SHIFT_MASK)) g->shift_hold = TRUE; if(dt_modifiers_include(state, GDK_CONTROL_MASK)) g->ctrl_hold = TRUE; } return 1; } else return 0; } GSList *mouse_actions(dt_iop_module_t *self) { GSList *lm = NULL; lm = dt_mouse_action_create_format(lm, DT_MOUSE_ACTION_LEFT_DRAG, 0, _("[%s on borders] crop"), self->name()); lm = dt_mouse_action_create_format(lm, DT_MOUSE_ACTION_LEFT_DRAG, GDK_SHIFT_MASK, _("[%s on borders] crop keeping ratio"), self->name()); lm = dt_mouse_action_create_format(lm, DT_MOUSE_ACTION_RIGHT_DRAG, 0, _("[%s] define/rotate horizon"), self->name()); return lm; } #undef PHI #undef INVPHI // 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