/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * * pikaseamlessclonetool.c * Copyright (C) 2011 Barak Itkin * * This program 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. * * This program 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 this program. If not, see . */ #include "config.h" #include #include /* gegl_operation_invalidate() */ #include #include #include "libpikabase/pikabase.h" #include "libpikawidgets/pikawidgets.h" #include "tools-types.h" #include "config/pikaguiconfig.h" /* playground */ #include "gegl/pika-gegl-utils.h" #include "core/pika.h" #include "core/pikabuffer.h" #include "core/pikadrawablefilter.h" #include "core/pikaimage.h" #include "core/pikaitem.h" #include "core/pikaprogress.h" #include "core/pikaprojection.h" #include "core/pikatoolinfo.h" #include "widgets/pikahelp-ids.h" #include "widgets/pikaclipboard.h" #include "display/pikadisplay.h" #include "display/pikadisplayshell.h" #include "display/pikadisplayshell-transform.h" #include "pikaseamlessclonetool.h" #include "pikaseamlesscloneoptions.h" #include "pikatoolcontrol.h" #include "pika-intl.h" #define SC_DEBUG TRUE #ifdef SC_DEBUG #define sc_debug_fstart() g_debug ("%s::start", __FUNCTION__); #define sc_debug_fend() g_debug ("%s::end", __FUNCTION__); #else #define sc_debug_fstart() #define sc_debug_fend() #endif #define pika_seamless_clone_tool_is_in_paste(sc,x0,y0) \ ( ((sc)->xoff <= (x0) && (x0) < (sc)->xoff + (sc)->width) \ && ((sc)->yoff <= (y0) && (y0) < (sc)->yoff + (sc)->height)) \ #define pika_seamless_clone_tool_is_in_paste_c(sc,coords) \ pika_seamless_clone_tool_is_in_paste((sc),(coords)->x,(coords)->y) /* init ----------> preprocess * | | * | | * | | * | v * | render(wait, motion) * | / | * | _____/ | * | _____/ | * v v v * quit <---------- commit * * Begin at INIT state * * INIT: Wait for click on canvas * have a paste ? -> PREPROCESS : -> QUIT * * PREPROCESS: Do the preprocessing * -> RENDER * * RENDER: Interact and wait for quit signal * commit quit ? -> COMMIT : -> QUIT * * COMMIT: Commit the changes * -> QUIT * * QUIT: Invoked by sending a ACTION_HALT to the tool_control * Free resources */ enum { SC_STATE_INIT, SC_STATE_PREPROCESS, SC_STATE_RENDER_WAIT, SC_STATE_RENDER_MOTION, SC_STATE_COMMIT, SC_STATE_QUIT }; static void pika_seamless_clone_tool_control (PikaTool *tool, PikaToolAction action, PikaDisplay *display); static void pika_seamless_clone_tool_button_press (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonPressType press_type, PikaDisplay *display); static void pika_seamless_clone_tool_button_release (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonReleaseType release_type, PikaDisplay *display); static void pika_seamless_clone_tool_motion (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaDisplay *display); static gboolean pika_seamless_clone_tool_key_press (PikaTool *tool, GdkEventKey *kevent, PikaDisplay *display); static void pika_seamless_clone_tool_oper_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, gboolean proximity, PikaDisplay *display); static void pika_seamless_clone_tool_cursor_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, PikaDisplay *display); static void pika_seamless_clone_tool_options_notify (PikaTool *tool, PikaToolOptions *options, const GParamSpec *pspec); static void pika_seamless_clone_tool_draw (PikaDrawTool *draw_tool); static void pika_seamless_clone_tool_start (PikaSeamlessCloneTool *sc, PikaDisplay *display); static void pika_seamless_clone_tool_stop (PikaSeamlessCloneTool *sc, gboolean display_change_only); static void pika_seamless_clone_tool_commit (PikaSeamlessCloneTool *sc); static void pika_seamless_clone_tool_create_render_node (PikaSeamlessCloneTool *sc); static gboolean pika_seamless_clone_tool_render_node_update (PikaSeamlessCloneTool *sc); static void pika_seamless_clone_tool_create_filter (PikaSeamlessCloneTool *sc, PikaDrawable *drawable); static void pika_seamless_clone_tool_filter_flush (PikaDrawableFilter *filter, PikaTool *tool); static void pika_seamless_clone_tool_filter_update (PikaSeamlessCloneTool *sc); G_DEFINE_TYPE (PikaSeamlessCloneTool, pika_seamless_clone_tool, PIKA_TYPE_DRAW_TOOL) #define parent_class pika_seamless_clone_tool_parent_class void pika_seamless_clone_tool_register (PikaToolRegisterCallback callback, gpointer data) { /* we should not know that "data" is a Gimp*, but what the heck this * is experimental playground stuff */ if (PIKA_GUI_CONFIG (PIKA (data)->config)->playground_seamless_clone_tool) (* callback) (PIKA_TYPE_SEAMLESS_CLONE_TOOL, PIKA_TYPE_SEAMLESS_CLONE_OPTIONS, pika_seamless_clone_options_gui, 0, "pika-seamless-clone-tool", _("Seamless Clone"), _("Seamless Clone: Seamlessly paste one image into another"), N_("_Seamless Clone"), NULL, NULL, PIKA_HELP_TOOL_SEAMLESS_CLONE, PIKA_ICON_TOOL_SEAMLESS_CLONE, data); } static void pika_seamless_clone_tool_class_init (PikaSeamlessCloneToolClass *klass) { PikaToolClass *tool_class = PIKA_TOOL_CLASS (klass); PikaDrawToolClass *draw_tool_class = PIKA_DRAW_TOOL_CLASS (klass); tool_class->control = pika_seamless_clone_tool_control; tool_class->button_press = pika_seamless_clone_tool_button_press; tool_class->button_release = pika_seamless_clone_tool_button_release; tool_class->motion = pika_seamless_clone_tool_motion; tool_class->key_press = pika_seamless_clone_tool_key_press; tool_class->oper_update = pika_seamless_clone_tool_oper_update; tool_class->cursor_update = pika_seamless_clone_tool_cursor_update; tool_class->options_notify = pika_seamless_clone_tool_options_notify; draw_tool_class->draw = pika_seamless_clone_tool_draw; } static void pika_seamless_clone_tool_init (PikaSeamlessCloneTool *self) { PikaTool *tool = PIKA_TOOL (self); pika_tool_control_set_dirty_mask (tool->control, PIKA_DIRTY_IMAGE | PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_DRAWABLE | PIKA_DIRTY_SELECTION); pika_tool_control_set_tool_cursor (tool->control, PIKA_TOOL_CURSOR_MOVE); self->tool_state = SC_STATE_INIT; } static void pika_seamless_clone_tool_control (PikaTool *tool, PikaToolAction action, PikaDisplay *display) { PikaSeamlessCloneTool *sc = PIKA_SEAMLESS_CLONE_TOOL (tool); switch (action) { case PIKA_TOOL_ACTION_PAUSE: case PIKA_TOOL_ACTION_RESUME: break; case PIKA_TOOL_ACTION_HALT: if (tool->display) pika_seamless_clone_tool_stop (sc, FALSE); /* TODO: If we have any tool options that should be reset, here is * a good place to do so. */ break; case PIKA_TOOL_ACTION_COMMIT: pika_seamless_clone_tool_commit (sc); break; } PIKA_TOOL_CLASS (parent_class)->control (tool, action, display); } /** * pika_seamless_clone_tool_start: * @sc: The PikaSeamlessCloneTool to initialize for usage on the given * display * @display: The display to initialize the tool for * * A utility function to initialize a tool for working on a given * display. At the beginning of each function, we can check if the event's * display is the same as the tool's one, and if not call this. This is * not required by the pikatool interface or anything like that, but * this is a convenient way to do all the initialization work in one * place, and this is how the base class (PikaDrawTool) does that */ static void pika_seamless_clone_tool_start (PikaSeamlessCloneTool *sc, PikaDisplay *display) { PikaTool *tool = PIKA_TOOL (sc); PikaImage *image = pika_display_get_image (display); GList *drawables = pika_image_get_selected_drawables (image); PikaDrawable *drawable; g_return_if_fail (g_list_length (drawables) == 1); drawable = drawables->data; g_list_free (drawables); /* First handle the paste - we need to make sure we have one in order * to do anything else. */ if (sc->paste == NULL) { PikaBuffer *buffer = pika_clipboard_get_buffer (tool->tool_info->pika); if (! buffer) { pika_tool_push_status (tool, display, "%s", _("There is no image data in the clipboard to paste.")); return; } sc->paste = pika_gegl_buffer_dup (pika_buffer_get_buffer (buffer)); g_object_unref (buffer); sc->width = gegl_buffer_get_width (sc->paste); sc->height = gegl_buffer_get_height (sc->paste); } /* Free resources which are relevant only for the previous display */ pika_seamless_clone_tool_stop (sc, TRUE); tool->display = display; pika_seamless_clone_tool_create_filter (sc, drawable); pika_draw_tool_start (PIKA_DRAW_TOOL (sc), display); sc->tool_state = SC_STATE_RENDER_WAIT; } /** * pika_seamless_clone_tool_stop: * @sc: The seamless clone tool whose resources should be freed * @display_change_only: Mark that the only reason for this call was a * switch of the working display. * * This function frees any resources associated with the seamless clone * tool, including caches, gegl graphs, and anything the tool created. * Afterwards, it initializes all the relevant pointers to some initial * value (usually NULL) like the init function does. * * Note that for seamless cloning, no change needs to be done when * switching to a different display, except for clearing the image map. * So for that, we provide a boolean parameter to specify that the only * change was one of the display */ static void pika_seamless_clone_tool_stop (PikaSeamlessCloneTool *sc, gboolean display_change_only) { /* See if we actually have any reason to stop */ if (sc->tool_state == SC_STATE_INIT) return; if (! display_change_only) { sc->tool_state = SC_STATE_INIT; g_clear_object (&sc->paste); g_clear_object (&sc->render_node); sc->sc_node = NULL; } /* This should always happen, even when we just switch a display */ if (sc->filter) { pika_drawable_filter_abort (sc->filter); g_clear_object (&sc->filter); if (PIKA_TOOL (sc)->display) pika_image_flush (pika_display_get_image (PIKA_TOOL (sc)->display)); } pika_draw_tool_stop (PIKA_DRAW_TOOL (sc)); } static void pika_seamless_clone_tool_commit (PikaSeamlessCloneTool *sc) { PikaTool *tool = PIKA_TOOL (sc); if (sc->filter) { pika_tool_control_push_preserve (tool->control, TRUE); pika_drawable_filter_commit (sc->filter, PIKA_PROGRESS (tool), FALSE); g_clear_object (&sc->filter); pika_tool_control_pop_preserve (tool->control); pika_image_flush (pika_display_get_image (tool->display)); } } static void pika_seamless_clone_tool_button_press (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonPressType press_type, PikaDisplay *display) { PikaSeamlessCloneTool *sc = PIKA_SEAMLESS_CLONE_TOOL (tool); if (display != tool->display) { pika_seamless_clone_tool_start (sc, display); /* Center the paste on the mouse */ sc->xoff = (gint) coords->x - sc->width / 2; sc->yoff = (gint) coords->y - sc->height / 2; } if (sc->tool_state == SC_STATE_RENDER_WAIT && pika_seamless_clone_tool_is_in_paste_c (sc, coords)) { pika_draw_tool_pause (PIKA_DRAW_TOOL (sc)); /* Record previous location, in case the user cancels the * movement */ sc->xoff_p = sc->xoff; sc->yoff_p = sc->yoff; /* Record the mouse location, so that the dragging offset can be * calculated */ sc->xclick = coords->x; sc->yclick = coords->y; pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); if (pika_seamless_clone_tool_render_node_update (sc)) { pika_seamless_clone_tool_filter_update (sc); } sc->tool_state = SC_STATE_RENDER_MOTION; /* In order to receive motion events from the current click, we must * activate the tool control */ pika_tool_control_activate (tool->control); } } void pika_seamless_clone_tool_button_release (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonReleaseType release_type, PikaDisplay *display) { PikaSeamlessCloneTool *sc = PIKA_SEAMLESS_CLONE_TOOL (tool); pika_tool_control_halt (tool->control); /* There is nothing to do, unless we were actually moving a paste */ if (sc->tool_state == SC_STATE_RENDER_MOTION) { pika_draw_tool_pause (PIKA_DRAW_TOOL (sc)); if (release_type == PIKA_BUTTON_RELEASE_CANCEL) { sc->xoff = sc->xoff_p; sc->yoff = sc->yoff_p; } else { sc->xoff = sc->xoff_p + (gint) (coords->x - sc->xclick); sc->yoff = sc->yoff_p + (gint) (coords->y - sc->yclick); } pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); if (pika_seamless_clone_tool_render_node_update (sc)) { pika_seamless_clone_tool_filter_update (sc); } sc->tool_state = SC_STATE_RENDER_WAIT; } } static void pika_seamless_clone_tool_motion (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaDisplay *display) { PikaSeamlessCloneTool *sc = PIKA_SEAMLESS_CLONE_TOOL (tool); if (sc->tool_state == SC_STATE_RENDER_MOTION) { pika_draw_tool_pause (PIKA_DRAW_TOOL (sc)); sc->xoff = sc->xoff_p + (gint) (coords->x - sc->xclick); sc->yoff = sc->yoff_p + (gint) (coords->y - sc->yclick); pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); if (pika_seamless_clone_tool_render_node_update (sc)) { pika_seamless_clone_tool_filter_update (sc); } } } static gboolean pika_seamless_clone_tool_key_press (PikaTool *tool, GdkEventKey *kevent, PikaDisplay *display) { PikaSeamlessCloneTool *sct = PIKA_SEAMLESS_CLONE_TOOL (tool); if (sct->tool_state == SC_STATE_RENDER_MOTION || sct->tool_state == SC_STATE_RENDER_WAIT) { switch (kevent->keyval) { case GDK_KEY_Return: case GDK_KEY_KP_Enter: case GDK_KEY_ISO_Enter: pika_tool_control_set_preserve (tool->control, TRUE); /* TODO: there may be issues with committing the image map * result after some changes were made and the preview * was scrolled. We can fix these by either invalidating * the area which is a union of the previous paste * rectangle each time (in the update function) or by * invalidating and re-rendering all now (expensive and * perhaps useless */ pika_drawable_filter_commit (sct->filter, PIKA_PROGRESS (tool), FALSE); g_clear_object (&sct->filter); pika_tool_control_set_preserve (tool->control, FALSE); pika_image_flush (pika_display_get_image (display)); pika_seamless_clone_tool_control (tool, PIKA_TOOL_ACTION_HALT, display); return TRUE; case GDK_KEY_Escape: pika_seamless_clone_tool_control (tool, PIKA_TOOL_ACTION_HALT, display); return TRUE; default: break; } } return FALSE; } static void pika_seamless_clone_tool_oper_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, gboolean proximity, PikaDisplay *display) { pika_draw_tool_pause (PIKA_DRAW_TOOL (tool)); /* TODO: Modify data here */ pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); } /* Mouse cursor policy: * - Always use the move cursor * - While dragging the paste, use a move modified * - Else, While hovering above it, display no modifier * - Else, display a "bad" modifier */ static void pika_seamless_clone_tool_cursor_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, PikaDisplay *display) { PikaSeamlessCloneTool *sc = PIKA_SEAMLESS_CLONE_TOOL (tool); PikaCursorModifier modifier = PIKA_CURSOR_MODIFIER_BAD; /* Only update if the tool is actually active on some display */ if (tool->display) { if (sc->tool_state == SC_STATE_RENDER_MOTION) { modifier = PIKA_CURSOR_MODIFIER_MOVE; } else if (sc->tool_state == SC_STATE_RENDER_WAIT && pika_seamless_clone_tool_is_in_paste_c (sc, coords)) { modifier = PIKA_CURSOR_MODIFIER_NONE; } pika_tool_control_set_cursor_modifier (tool->control, modifier); } PIKA_TOOL_CLASS (parent_class)->cursor_update (tool, coords, state, display); } static void pika_seamless_clone_tool_options_notify (PikaTool *tool, PikaToolOptions *options, const GParamSpec *pspec) { PikaSeamlessCloneTool *sc = PIKA_SEAMLESS_CLONE_TOOL (tool); PIKA_TOOL_CLASS (parent_class)->options_notify (tool, options, pspec); if (! tool->display) return; pika_draw_tool_pause (PIKA_DRAW_TOOL (tool)); if (! strcmp (pspec->name, "max-refine-scale")) { if (pika_seamless_clone_tool_render_node_update (sc)) { pika_seamless_clone_tool_filter_update (sc); } } pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); } static void pika_seamless_clone_tool_draw (PikaDrawTool *draw_tool) { PikaSeamlessCloneTool *sc = PIKA_SEAMLESS_CLONE_TOOL (draw_tool); if (sc->tool_state == SC_STATE_RENDER_WAIT || sc->tool_state == SC_STATE_RENDER_MOTION) { pika_draw_tool_add_rectangle (draw_tool, FALSE, sc->xoff, sc->yoff, sc->width, sc->height); } } /** * pika_seamless_clone_tool_create_render_node: * @sc: The PikaSeamlessCloneTool to initialize * * This function creates a Gegl node graph of the composition which is * needed to render the drawable. The graph should have an "input" pad * which will receive the drawable on which the preview is applied, and * it should also have an "output" pad to which the final result will be * rendered */ static void pika_seamless_clone_tool_create_render_node (PikaSeamlessCloneTool *sc) { /* Here is a textual description of the graph we are going to create: * * <- drawable * +--+--------------------------+ * | |output | * | | | * | | <- paste | * | | |output | * | | | | * | |input |aux | * | | * | |output | * | | | * | |input | * +----+------------------------+ * */ PikaSeamlessCloneOptions *options = PIKA_SEAMLESS_CLONE_TOOL_GET_OPTIONS (sc); GeglNode *node; GeglNode *op, *paste, *overlay; GeglNode *input, *output; node = gegl_node_new (); input = gegl_node_get_input_proxy (node, "input"); output = gegl_node_get_output_proxy (node, "output"); paste = gegl_node_new_child (node, "operation", "gegl:buffer-source", "buffer", sc->paste, NULL); op = gegl_node_new_child (node, "operation", "gegl:seamless-clone", "max-refine-scale", options->max_refine_scale, NULL); overlay = gegl_node_new_child (node, "operation", "svg:dst-over", NULL); gegl_node_link_many (input, op, overlay, output, NULL); gegl_node_connect (paste, "output", op, "aux"); gegl_node_connect (input, "output", overlay, "aux"); sc->render_node = node; sc->sc_node = op; } /* pika_seamless_clone_tool_render_node_update: * sc: the Seamless Clone tool whose render has to be updated. * * Returns: TRUE if any property changed. */ static gboolean pika_seamless_clone_tool_render_node_update (PikaSeamlessCloneTool *sc) { static gint rendered__max_refine_scale = -1; static gint rendered_xoff = G_MAXINT; static gint rendered_yoff = G_MAXINT; PikaSeamlessCloneOptions *options = PIKA_SEAMLESS_CLONE_TOOL_GET_OPTIONS (sc); PikaDrawable *bg = PIKA_TOOL (sc)->drawables->data; gint off_x, off_y; /* All properties stay the same. No need to update. */ if (rendered__max_refine_scale == options->max_refine_scale && rendered_xoff == sc->xoff && rendered_yoff == sc->yoff) return FALSE; pika_item_get_offset (PIKA_ITEM (bg), &off_x, &off_y); gegl_node_set (sc->sc_node, "xoff", (gint) sc->xoff - off_x, "yoff", (gint) sc->yoff - off_y, "max-refine-scale", (gint) options->max_refine_scale, NULL); rendered__max_refine_scale = options->max_refine_scale; rendered_xoff = sc->xoff; rendered_yoff = sc->yoff; return TRUE; } static void pika_seamless_clone_tool_create_filter (PikaSeamlessCloneTool *sc, PikaDrawable *drawable) { if (! sc->render_node) pika_seamless_clone_tool_create_render_node (sc); sc->filter = pika_drawable_filter_new (drawable, _("Seamless Clone"), sc->render_node, PIKA_ICON_TOOL_SEAMLESS_CLONE); pika_drawable_filter_set_region (sc->filter, PIKA_FILTER_REGION_DRAWABLE); g_signal_connect (sc->filter, "flush", G_CALLBACK (pika_seamless_clone_tool_filter_flush), sc); } static void pika_seamless_clone_tool_filter_flush (PikaDrawableFilter *filter, PikaTool *tool) { PikaImage *image = pika_display_get_image (tool->display); pika_projection_flush (pika_image_get_projection (image)); } static void pika_seamless_clone_tool_filter_update (PikaSeamlessCloneTool *sc) { PikaTool *tool = PIKA_TOOL (sc); PikaDisplayShell *shell = pika_display_get_shell (tool->display); PikaItem *item = PIKA_ITEM (tool->drawables->data); gint x, y; gint w, h; gint off_x, off_y; GeglRectangle visible; GeglOperation *op = NULL; PikaProgress *progress; GeglNode *output; GeglProcessor *processor; gdouble value; progress = pika_progress_start (PIKA_PROGRESS (sc), FALSE, _("Cloning the foreground object")); /* Find out at which x,y is the top left corner of the currently * displayed part */ pika_display_shell_untransform_viewport (shell, ! shell->show_all, &x, &y, &w, &h); /* Find out where is our drawable positioned */ pika_item_get_offset (item, &off_x, &off_y); /* Create a rectangle from the intersection of the currently displayed * part with the drawable */ pika_rectangle_intersect (x, y, w, h, off_x, off_y, pika_item_get_width (item), pika_item_get_height (item), &visible.x, &visible.y, &visible.width, &visible.height); /* Since the filter_apply function receives a rectangle describing * where it should update the preview, and since that rectangle should * be relative to the drawable's location, we now offset back by the * drawable's offsetts. */ visible.x -= off_x; visible.y -= off_y; g_object_get (sc->sc_node, "gegl-operation", &op, NULL); /* If any cache of the visible area was present, clear it! * We need to clear the cache in the sc_node, since that is * where the previous paste was located */ gegl_operation_invalidate (op, &visible, TRUE); g_object_unref (op); /* Now update the image map and show this area */ pika_drawable_filter_apply (sc->filter, NULL); /* Show update progress. */ output = gegl_node_get_output_proxy (sc->render_node, "output"); processor = gegl_node_new_processor (output, NULL); while (gegl_processor_work (processor, &value)) { if (progress) pika_progress_set_value (progress, value); } if (progress) pika_progress_end (progress); g_object_unref (processor); }