mirror of
https://gitlab.gnome.org/GNOME/gimp.git
synced 2025-10-06 01:12:40 +02:00
This commit will make all transform tools run on a link layer cumulate their transform matrix on top of the previous transform steps. It means that as long as you don't edit the pixels another way (e.g. with a paint tool), all your transformations will apply as a single transformation. For instance it means that applying several transform tool steps on a monitored link layer will be less destructive than applying the exact same transformations on the exact same "normal" layer contents. Even scaling an image to 1x1 then back to a big size will work very fine! Note nevertheless the following limitations in current implementation: * The link layer with transformations will still show as a standard link layer. Nothing says it has transformation applied on it right now. * To drop transformations applied on a link layer, you have to discard the link info, then monitor the link again. A specific action in the contextual menu could be worth it. * This should work with all transform tools (scale, rotation, unified, perspective, 3D, even Warp…) but it won't work for the Flip tool, nor will it work for the Transform actions. I will need to implement GimpItem's rotate(), flip() and resize() methods next. * The layer mask would still be destructively transformed (I have not made any tests with layer masks yet, but this should be done next too). * I think that the "scaled-only" property is now meaningless. It is now being replaced by the presence of the GimpMatrix3. Nevertheless I have still not removed this property. * The load/save code has not been redone yet to include all these changes. The kind of caveats we'd have to know about (and which are not planned for change, because it's just how it is): * Any intermediate interpolation methods are dropped when cumulating transform steps. Only the last interpolation is stored. This is because anyway the interpolation is only there as the best algorithm where we visually see the less quality loss. Applying several transformations as a single matrix will always be visually better than applying several matrices (whatever the intermediate interpolation methods chosen). * This only works with the "Adjust" clipping method (basically no clipping) because 2 transform steps with clipping won't produce the same result as the multiplied matrix with clipping. It means that applying a transform with clipping will downgrade your link layer to being a normal layer. The only issue I have with this is how to best convey that clipping is a major setting setting here, which disables our less-destructive abilities. Right now, people will just have to "know" it. * Vector link layers in particular will have 2 levels of non/less-destructivity transforms. In particular any scaling (both through "Scale Image", "Scale Layers" and transform tools — since I added code to detect a matrix doing only scaling and optionally translation) done **first** will be completely non-destructive since we will simply reload the original vector source at the right dimensions. Any other kind of transforms will be appended through the cumulative matrix, as raster link layers. This also includes scaling done **after** other transforms, since we cannot easily move the scaling first (matrix multiplication is not commutative). This second level of scaling will therefore be *less* destructive, but still destructive. It is possible eventually to improve the whole thing if we add some day the ability to request loading a vector image with a transform matrix (it will then be up to each vector format plug-in to support this feature or not). Note: it could be argued that this whole implementation could in fact be moved over to base layers, since it would allow also less-destructivity when applying multiple transformations in a row. We would only merge results once we edit pixels more directly. But I think that it's not a bad idea to experiment with this new code in the link layer. Eventually I may likely move this to the parent GimpLayer if no blocking issues are found.
976 lines
31 KiB
C
976 lines
31 KiB
C
/* GIMP - The GNU Image Manipulation Program
|
|
* Copyright (C) 1995-2001 Spencer Kimball, Peter Mattis, and others
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <gegl.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include "libgimpmath/gimpmath.h"
|
|
#include "libgimpwidgets/gimpwidgets.h"
|
|
|
|
#include "tools-types.h"
|
|
|
|
#include "config/gimpguiconfig.h"
|
|
|
|
#include "core/gimp.h"
|
|
#include "core/gimpdrawable-transform.h"
|
|
#include "core/gimperror.h"
|
|
#include "core/gimpimage.h"
|
|
#include "core/gimpimage-item-list.h"
|
|
#include "core/gimpimage-transform.h"
|
|
#include "core/gimpimage-undo.h"
|
|
#include "core/gimplayer.h"
|
|
#include "core/gimplayermask.h"
|
|
#include "core/gimpprogress.h"
|
|
#include "core/gimp-transform-resize.h"
|
|
|
|
#include "path/gimppath.h"
|
|
|
|
#include "display/gimpdisplay.h"
|
|
#include "display/gimpdisplayshell.h"
|
|
|
|
#include "widgets/gimpmessagedialog.h"
|
|
#include "widgets/gimpmessagebox.h"
|
|
#include "widgets/gimpwidgets-utils.h"
|
|
|
|
#include "gimptoolcontrol.h"
|
|
#include "gimptools-utils.h"
|
|
#include "gimptransformoptions.h"
|
|
#include "gimptransformtool.h"
|
|
|
|
#include "gimp-intl.h"
|
|
|
|
|
|
/* the minimal ratio between the transformed item size and the image size,
|
|
* above which confirmation is required.
|
|
*/
|
|
#define MIN_CONFIRMATION_RATIO 10
|
|
|
|
|
|
/* local function prototypes */
|
|
|
|
static void gimp_transform_tool_control (GimpTool *tool,
|
|
GimpToolAction action,
|
|
GimpDisplay *display);
|
|
|
|
static gchar * gimp_transform_tool_real_get_undo_desc (GimpTransformTool *tr_tool);
|
|
static GimpTransformDirection gimp_transform_tool_real_get_direction (GimpTransformTool *tr_tool);
|
|
static GeglBuffer * gimp_transform_tool_real_transform (GimpTransformTool *tr_tool,
|
|
GList *objects,
|
|
GeglBuffer *orig_buffer,
|
|
gint orig_offset_x,
|
|
gint orig_offset_y,
|
|
GimpColorProfile **buffer_profile,
|
|
gint *new_offset_x,
|
|
gint *new_offset_y);
|
|
|
|
static void gimp_transform_tool_halt (GimpTransformTool *tr_tool);
|
|
|
|
static gboolean gimp_transform_tool_confirm (GimpTransformTool *tr_tool,
|
|
GimpDisplay *display);
|
|
|
|
|
|
G_DEFINE_TYPE (GimpTransformTool, gimp_transform_tool, GIMP_TYPE_DRAW_TOOL)
|
|
|
|
#define parent_class gimp_transform_tool_parent_class
|
|
|
|
|
|
/* private functions */
|
|
|
|
|
|
static void
|
|
gimp_transform_tool_class_init (GimpTransformToolClass *klass)
|
|
{
|
|
GimpToolClass *tool_class = GIMP_TOOL_CLASS (klass);
|
|
|
|
tool_class->control = gimp_transform_tool_control;
|
|
|
|
klass->recalc_matrix = NULL;
|
|
klass->get_undo_desc = gimp_transform_tool_real_get_undo_desc;
|
|
klass->get_direction = gimp_transform_tool_real_get_direction;
|
|
klass->transform = gimp_transform_tool_real_transform;
|
|
|
|
klass->undo_desc = _("Transform");
|
|
klass->progress_text = _("Transforming");
|
|
}
|
|
|
|
static void
|
|
gimp_transform_tool_init (GimpTransformTool *tr_tool)
|
|
{
|
|
gimp_matrix3_identity (&tr_tool->transform);
|
|
tr_tool->transform_valid = TRUE;
|
|
|
|
tr_tool->restore_type = FALSE;
|
|
}
|
|
|
|
static void
|
|
gimp_transform_tool_control (GimpTool *tool,
|
|
GimpToolAction action,
|
|
GimpDisplay *display)
|
|
{
|
|
GimpTransformTool *tr_tool = GIMP_TRANSFORM_TOOL (tool);
|
|
|
|
switch (action)
|
|
{
|
|
case GIMP_TOOL_ACTION_PAUSE:
|
|
case GIMP_TOOL_ACTION_RESUME:
|
|
break;
|
|
|
|
case GIMP_TOOL_ACTION_HALT:
|
|
gimp_transform_tool_halt (tr_tool);
|
|
break;
|
|
|
|
case GIMP_TOOL_ACTION_COMMIT:
|
|
break;
|
|
}
|
|
|
|
GIMP_TOOL_CLASS (parent_class)->control (tool, action, display);
|
|
}
|
|
|
|
static gchar *
|
|
gimp_transform_tool_real_get_undo_desc (GimpTransformTool *tr_tool)
|
|
{
|
|
return g_strdup (GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->undo_desc);
|
|
}
|
|
|
|
static GimpTransformDirection
|
|
gimp_transform_tool_real_get_direction (GimpTransformTool *tr_tool)
|
|
{
|
|
GimpTransformOptions *options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tr_tool);
|
|
|
|
return options->direction;
|
|
}
|
|
|
|
static GeglBuffer *
|
|
gimp_transform_tool_real_transform (GimpTransformTool *tr_tool,
|
|
GList *objects,
|
|
GeglBuffer *orig_buffer,
|
|
gint orig_offset_x,
|
|
gint orig_offset_y,
|
|
GimpColorProfile **buffer_profile,
|
|
gint *new_offset_x,
|
|
gint *new_offset_y)
|
|
{
|
|
GimpTransformToolClass *klass = GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool);
|
|
GimpTool *tool = GIMP_TOOL (tr_tool);
|
|
GimpTransformOptions *options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tool);
|
|
GimpContext *context = GIMP_CONTEXT (options);
|
|
GeglBuffer *ret = NULL;
|
|
GimpTransformResize clip = options->clip;
|
|
GimpTransformDirection direction;
|
|
GimpProgress *progress;
|
|
|
|
direction = GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->get_direction (tr_tool);
|
|
|
|
progress = gimp_progress_start (GIMP_PROGRESS (tool), FALSE,
|
|
"%s", klass->progress_text);
|
|
|
|
while (g_main_context_pending (NULL))
|
|
g_main_context_iteration (NULL, FALSE);
|
|
|
|
if (orig_buffer)
|
|
{
|
|
/* this happens when transforming a selection cut out of
|
|
* normal drawables.
|
|
*/
|
|
|
|
ret = gimp_drawable_transform_buffer_affine (objects->data,
|
|
context,
|
|
orig_buffer,
|
|
orig_offset_x,
|
|
orig_offset_y,
|
|
&tr_tool->transform,
|
|
direction,
|
|
options->interpolation,
|
|
clip,
|
|
buffer_profile,
|
|
new_offset_x,
|
|
new_offset_y,
|
|
progress);
|
|
}
|
|
else if (g_list_length (objects) == 1 && GIMP_IS_IMAGE (objects->data))
|
|
{
|
|
/* this happens for images */
|
|
|
|
gimp_image_transform (objects->data, context,
|
|
&tr_tool->transform,
|
|
direction,
|
|
options->interpolation,
|
|
clip,
|
|
progress);
|
|
}
|
|
else
|
|
{
|
|
GList *items;
|
|
|
|
/* this happens for entire drawables, paths and layer groups */
|
|
g_return_val_if_fail (g_list_length (objects) > 0, NULL);
|
|
|
|
items = gimp_image_item_list_filter (g_list_copy (objects));
|
|
|
|
gimp_image_item_list_transform (gimp_item_get_image (objects->data),
|
|
items, context,
|
|
&tr_tool->transform,
|
|
direction,
|
|
options->interpolation,
|
|
clip,
|
|
progress);
|
|
g_list_free (items);
|
|
}
|
|
|
|
if (progress)
|
|
gimp_progress_end (progress);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
gimp_transform_tool_halt (GimpTransformTool *tr_tool)
|
|
{
|
|
GimpTransformOptions *options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tr_tool);
|
|
|
|
tr_tool->x1 = 0;
|
|
tr_tool->y1 = 0;
|
|
tr_tool->x2 = 0;
|
|
tr_tool->y2 = 0;
|
|
|
|
if (tr_tool->restore_type)
|
|
{
|
|
g_object_set (options,
|
|
"type", tr_tool->saved_type,
|
|
NULL);
|
|
|
|
tr_tool->restore_type = FALSE;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gimp_transform_tool_confirm (GimpTransformTool *tr_tool,
|
|
GimpDisplay *display)
|
|
{
|
|
GimpTransformOptions *options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tr_tool);
|
|
GimpDisplayShell *shell = gimp_display_get_shell (display);
|
|
GimpImage *image = gimp_display_get_image (display);
|
|
GList *selected_objects;
|
|
gdouble max_ratio = 0.0;
|
|
GimpObject *max_ratio_object = NULL;
|
|
|
|
selected_objects = gimp_transform_tool_get_selected_objects (tr_tool, display);
|
|
|
|
if (GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->recalc_matrix)
|
|
{
|
|
GimpMatrix3 transform;
|
|
GimpTransformDirection direction;
|
|
GeglRectangle selection_bounds;
|
|
gboolean selection_empty = TRUE;
|
|
GList *objects = NULL;
|
|
GList *iter;
|
|
|
|
transform = tr_tool->transform;
|
|
direction = GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->get_direction (
|
|
tr_tool);
|
|
|
|
if (direction == GIMP_TRANSFORM_BACKWARD)
|
|
gimp_matrix3_invert (&transform);
|
|
|
|
if (options->type == GIMP_TRANSFORM_TYPE_LAYER)
|
|
{
|
|
for (iter = selected_objects; iter; iter = iter->next)
|
|
if (! gimp_viewable_get_children (GIMP_VIEWABLE (iter->data)))
|
|
{
|
|
if (gimp_item_bounds (GIMP_ITEM (gimp_image_get_mask (image)),
|
|
&selection_bounds.x, &selection_bounds.y,
|
|
&selection_bounds.width, &selection_bounds.height))
|
|
{
|
|
selection_empty = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selection_empty &&
|
|
selected_objects &&
|
|
GIMP_IS_ITEM (selected_objects->data))
|
|
{
|
|
objects = gimp_image_item_list_filter (g_list_copy (selected_objects));
|
|
g_list_free (selected_objects);
|
|
}
|
|
else
|
|
{
|
|
objects = selected_objects;
|
|
}
|
|
|
|
if (options->type == GIMP_TRANSFORM_TYPE_IMAGE)
|
|
{
|
|
objects = g_list_concat (
|
|
objects,
|
|
gimp_image_item_list_get_list (image,
|
|
GIMP_ITEM_TYPE_ALL,
|
|
GIMP_ITEM_SET_ALL));
|
|
}
|
|
|
|
for (iter = objects; iter; iter = g_list_next (iter))
|
|
{
|
|
GimpObject *object = iter->data;
|
|
GimpTransformResize clip = options->clip;
|
|
GeglRectangle orig_bounds;
|
|
GeglRectangle new_bounds;
|
|
gdouble ratio = 0.0;
|
|
|
|
if (GIMP_IS_DRAWABLE (object))
|
|
{
|
|
if (selection_empty)
|
|
{
|
|
GimpItem *item = GIMP_ITEM (object);
|
|
|
|
gimp_item_get_offset (item, &orig_bounds.x, &orig_bounds.y);
|
|
|
|
orig_bounds.width = gimp_item_get_width (item);
|
|
orig_bounds.height = gimp_item_get_height (item);
|
|
|
|
clip = gimp_item_get_clip (item, clip);
|
|
}
|
|
else
|
|
{
|
|
orig_bounds = selection_bounds;
|
|
}
|
|
}
|
|
else if (GIMP_IS_ITEM (object))
|
|
{
|
|
GimpItem *item = GIMP_ITEM (object);
|
|
|
|
gimp_item_bounds (item,
|
|
&orig_bounds.x, &orig_bounds.y,
|
|
&orig_bounds.width, &orig_bounds.height);
|
|
|
|
clip = gimp_item_get_clip (item, clip);
|
|
}
|
|
else
|
|
{
|
|
GimpImage *image;
|
|
|
|
g_return_val_if_fail (GIMP_IS_IMAGE (object), FALSE);
|
|
|
|
image = GIMP_IMAGE (object);
|
|
|
|
orig_bounds.x = 0;
|
|
orig_bounds.y = 0;
|
|
orig_bounds.width = gimp_image_get_width (image);
|
|
orig_bounds.height = gimp_image_get_height (image);
|
|
}
|
|
|
|
gimp_transform_resize_boundary (&transform, clip,
|
|
|
|
orig_bounds.x,
|
|
orig_bounds.y,
|
|
orig_bounds.x + orig_bounds.width,
|
|
orig_bounds.y + orig_bounds.height,
|
|
|
|
&new_bounds.x,
|
|
&new_bounds.y,
|
|
&new_bounds.width,
|
|
&new_bounds.height);
|
|
|
|
new_bounds.width -= new_bounds.x;
|
|
new_bounds.height -= new_bounds.y;
|
|
|
|
if (new_bounds.width > orig_bounds.width)
|
|
{
|
|
ratio = MAX (ratio,
|
|
(gdouble) new_bounds.width /
|
|
(gdouble) gimp_image_get_width (image));
|
|
}
|
|
|
|
if (new_bounds.height > orig_bounds.height)
|
|
{
|
|
ratio = MAX (ratio,
|
|
(gdouble) new_bounds.height /
|
|
(gdouble) gimp_image_get_height (image));
|
|
}
|
|
|
|
if (ratio > max_ratio)
|
|
{
|
|
max_ratio = ratio;
|
|
max_ratio_object = object;
|
|
}
|
|
}
|
|
|
|
g_list_free (objects);
|
|
}
|
|
|
|
if (max_ratio > MIN_CONFIRMATION_RATIO)
|
|
{
|
|
GtkWidget *dialog;
|
|
gint response;
|
|
|
|
dialog = gimp_message_dialog_new (_("Confirm Transformation"),
|
|
GIMP_ICON_DIALOG_WARNING,
|
|
GTK_WIDGET (shell),
|
|
GTK_DIALOG_MODAL |
|
|
GTK_DIALOG_DESTROY_WITH_PARENT,
|
|
gimp_standard_help_func, NULL,
|
|
|
|
_("_Cancel"), GTK_RESPONSE_CANCEL,
|
|
_("_Transform"), GTK_RESPONSE_OK,
|
|
|
|
NULL);
|
|
|
|
gimp_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
|
|
GTK_RESPONSE_OK,
|
|
GTK_RESPONSE_CANCEL,
|
|
-1);
|
|
|
|
if (GIMP_IS_ITEM (max_ratio_object))
|
|
{
|
|
gimp_message_box_set_primary_text (GIMP_MESSAGE_DIALOG (dialog)->box,
|
|
_("Transformation creates "
|
|
"a very large item."));
|
|
|
|
gimp_message_box_set_text (
|
|
GIMP_MESSAGE_DIALOG (dialog)->box,
|
|
_("Applying the transformation will result "
|
|
"in an item that is over %g times larger "
|
|
"than the image."),
|
|
floor (max_ratio));
|
|
}
|
|
else
|
|
{
|
|
gimp_message_box_set_primary_text (GIMP_MESSAGE_DIALOG (dialog)->box,
|
|
_("Transformation creates "
|
|
"a very large image."));
|
|
|
|
gimp_message_box_set_text (
|
|
GIMP_MESSAGE_DIALOG (dialog)->box,
|
|
_("Applying the transformation will enlarge "
|
|
"the image by a factor of %g."),
|
|
floor (max_ratio));
|
|
}
|
|
|
|
response = gtk_dialog_run (GTK_DIALOG (dialog));
|
|
|
|
gtk_widget_destroy (dialog);
|
|
|
|
if (response != GTK_RESPONSE_OK)
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
/* public functions */
|
|
|
|
|
|
gboolean
|
|
gimp_transform_tool_bounds (GimpTransformTool *tr_tool,
|
|
GimpDisplay *display)
|
|
{
|
|
GimpTransformOptions *options;
|
|
GimpDisplayShell *shell;
|
|
GimpImage *image;
|
|
gboolean non_empty = TRUE;
|
|
|
|
g_return_val_if_fail (GIMP_IS_TRANSFORM_TOOL (tr_tool), FALSE);
|
|
|
|
options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tr_tool);
|
|
image = gimp_display_get_image (display);
|
|
shell = gimp_display_get_shell (display);
|
|
|
|
g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
|
|
|
|
switch (options->type)
|
|
{
|
|
case GIMP_TRANSFORM_TYPE_LAYER:
|
|
{
|
|
GList *drawables;
|
|
GList *iter;
|
|
gint x = G_MAXINT;
|
|
gint y = G_MAXINT;
|
|
gint width = G_MININT;
|
|
gint height = G_MININT;
|
|
|
|
drawables = gimp_image_get_selected_drawables (image);
|
|
non_empty = FALSE;
|
|
|
|
for (iter = drawables; iter; iter = iter->next)
|
|
{
|
|
gint offset_x;
|
|
gint offset_y;
|
|
gint mask_off_x;
|
|
gint mask_off_y;
|
|
gint mask_width;
|
|
gint mask_height;
|
|
gboolean has_pixels;
|
|
|
|
gimp_item_get_offset (GIMP_ITEM (iter->data), &offset_x,
|
|
&offset_y);
|
|
/* This is the bounds relatively to the drawable. */
|
|
has_pixels = gimp_item_mask_intersect (GIMP_ITEM (iter->data),
|
|
&mask_off_x, &mask_off_y,
|
|
&mask_width, &mask_height);
|
|
|
|
if (has_pixels)
|
|
non_empty = TRUE;
|
|
|
|
/* The bounds relatively to the image. */
|
|
x = MIN (offset_x + mask_off_x, x);
|
|
y = MIN (offset_y + mask_off_y, y);
|
|
width = MAX (offset_x + mask_off_x + mask_width, width);
|
|
height = MAX (offset_y + mask_off_y + mask_height, height);
|
|
}
|
|
|
|
tr_tool->x1 = x;
|
|
tr_tool->y1 = y;
|
|
tr_tool->x2 = width;
|
|
tr_tool->y2 = height;
|
|
|
|
g_list_free (drawables);
|
|
}
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_SELECTION:
|
|
{
|
|
gimp_item_bounds (GIMP_ITEM (gimp_image_get_mask (image)),
|
|
&tr_tool->x1, &tr_tool->y1,
|
|
&tr_tool->x2, &tr_tool->y2);
|
|
tr_tool->x2 += tr_tool->x1;
|
|
tr_tool->y2 += tr_tool->y1;
|
|
}
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_PATH:
|
|
{
|
|
GimpChannel *selection = gimp_image_get_mask (image);
|
|
|
|
/* if selection is not empty, use its bounds to perform the
|
|
* transformation of the path
|
|
*/
|
|
|
|
if (! gimp_channel_is_empty (selection))
|
|
{
|
|
gimp_item_bounds (GIMP_ITEM (selection),
|
|
&tr_tool->x1, &tr_tool->y1,
|
|
&tr_tool->x2, &tr_tool->y2);
|
|
|
|
tr_tool->x2 += tr_tool->x1;
|
|
tr_tool->y2 += tr_tool->y1;
|
|
}
|
|
else
|
|
{
|
|
GList *iter;
|
|
|
|
/* without selection, test the emptiness of the path bounds :
|
|
* if empty, use the canvas bounds
|
|
* else use the path bounds
|
|
*/
|
|
|
|
tr_tool->x1 = G_MAXINT;
|
|
tr_tool->y1 = G_MAXINT;
|
|
tr_tool->x2 = G_MININT;
|
|
tr_tool->y2 = G_MININT;
|
|
|
|
for (iter = gimp_image_get_selected_paths (image); iter; iter = iter->next)
|
|
{
|
|
GimpItem *item = iter->data;
|
|
gint x;
|
|
gint y;
|
|
gint width;
|
|
gint height;
|
|
|
|
if (gimp_item_bounds (item, &x, &y, &width, &height))
|
|
{
|
|
tr_tool->x1 = MIN (tr_tool->x1, x);
|
|
tr_tool->y1 = MIN (tr_tool->y1, y);
|
|
tr_tool->x2 = MAX (tr_tool->x2, x + width);
|
|
tr_tool->y2 = MAX (tr_tool->y2, y + height);
|
|
}
|
|
}
|
|
|
|
if (tr_tool->x2 <= tr_tool->x1 || tr_tool->y2 <= tr_tool->y1)
|
|
{
|
|
tr_tool->x1 = 0;
|
|
tr_tool->y1 = 0;
|
|
tr_tool->x2 = gimp_image_get_width (image);
|
|
tr_tool->y2 = gimp_image_get_height (image);
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_IMAGE:
|
|
if (! shell->show_all)
|
|
{
|
|
tr_tool->x1 = 0;
|
|
tr_tool->y1 = 0;
|
|
tr_tool->x2 = gimp_image_get_width (image);
|
|
tr_tool->y2 = gimp_image_get_height (image);
|
|
}
|
|
else
|
|
{
|
|
GeglRectangle bounding_box;
|
|
|
|
bounding_box = gimp_display_shell_get_bounding_box (shell);
|
|
|
|
tr_tool->x1 = bounding_box.x;
|
|
tr_tool->y1 = bounding_box.y;
|
|
tr_tool->x2 = bounding_box.x + bounding_box.width;
|
|
tr_tool->y2 = bounding_box.y + bounding_box.height;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return non_empty;
|
|
}
|
|
|
|
void
|
|
gimp_transform_tool_recalc_matrix (GimpTransformTool *tr_tool,
|
|
GimpDisplay *display)
|
|
{
|
|
g_return_if_fail (GIMP_IS_TRANSFORM_TOOL (tr_tool));
|
|
g_return_if_fail (GIMP_IS_DISPLAY (display));
|
|
|
|
if (tr_tool->x1 == tr_tool->x2 && tr_tool->y1 == tr_tool->y2)
|
|
gimp_transform_tool_bounds (tr_tool, display);
|
|
|
|
if (GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->recalc_matrix)
|
|
GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->recalc_matrix (tr_tool);
|
|
}
|
|
|
|
GList *
|
|
gimp_transform_tool_get_selected_objects (GimpTransformTool *tr_tool,
|
|
GimpDisplay *display)
|
|
{
|
|
GimpTransformOptions *options;
|
|
GimpImage *image;
|
|
GList *objects = NULL;
|
|
|
|
g_return_val_if_fail (GIMP_IS_TRANSFORM_TOOL (tr_tool), NULL);
|
|
g_return_val_if_fail (GIMP_IS_DISPLAY (display), NULL);
|
|
|
|
options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tr_tool);
|
|
|
|
image = gimp_display_get_image (display);
|
|
|
|
g_return_val_if_fail (GIMP_IS_IMAGE (image), NULL);
|
|
|
|
if (tr_tool->objects)
|
|
return g_list_copy (tr_tool->objects);
|
|
|
|
switch (options->type)
|
|
{
|
|
case GIMP_TRANSFORM_TYPE_LAYER:
|
|
objects = gimp_image_get_selected_drawables (image);
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_SELECTION:
|
|
if (! gimp_channel_is_empty (gimp_image_get_mask (image)))
|
|
objects = g_list_prepend (NULL, gimp_image_get_mask (image));
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_PATH:
|
|
objects = g_list_copy (gimp_image_get_selected_paths (image));
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_IMAGE:
|
|
objects = g_list_prepend (NULL, image);
|
|
break;
|
|
}
|
|
|
|
return objects;
|
|
}
|
|
|
|
GList *
|
|
gimp_transform_tool_check_selected_objects (GimpTransformTool *tr_tool,
|
|
GimpDisplay *display,
|
|
GError **error)
|
|
{
|
|
GimpTransformOptions *options;
|
|
GList *objects;
|
|
GList *iter;
|
|
const gchar *null_message = NULL;
|
|
const gchar *locked_message = NULL;
|
|
GimpItem *locked_item = NULL;
|
|
GimpGuiConfig *config = GIMP_GUI_CONFIG (display->gimp->config);
|
|
|
|
g_return_val_if_fail (GIMP_IS_TRANSFORM_TOOL (tr_tool), NULL);
|
|
g_return_val_if_fail (GIMP_IS_DISPLAY (display), NULL);
|
|
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
|
|
|
|
options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tr_tool);
|
|
|
|
objects = gimp_transform_tool_get_selected_objects (tr_tool, display);
|
|
|
|
switch (options->type)
|
|
{
|
|
case GIMP_TRANSFORM_TYPE_LAYER:
|
|
null_message = _("There is no layer to transform.");
|
|
|
|
for (iter = objects; iter; iter = iter->next)
|
|
{
|
|
GimpItem *item = iter->data;
|
|
|
|
if (gimp_item_is_content_locked (item, &locked_item))
|
|
locked_message = _("A selected layer's pixels are locked.");
|
|
else if (gimp_item_is_position_locked (item, &locked_item))
|
|
locked_message = _("A selected layer's position and size are locked.");
|
|
|
|
if (! gimp_item_is_visible (item) &&
|
|
! config->edit_non_visible &&
|
|
! g_list_find (tr_tool->objects, item)) /* see bug #759194 */
|
|
{
|
|
g_set_error_literal (error, GIMP_ERROR, GIMP_FAILED,
|
|
_("A selected layer is not visible."));
|
|
return NULL;
|
|
}
|
|
|
|
if (! gimp_transform_tool_bounds (tr_tool, display))
|
|
{
|
|
g_set_error_literal (error, GIMP_ERROR, GIMP_FAILED,
|
|
_("The selection does not intersect with a selected layer."));
|
|
return NULL;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_SELECTION:
|
|
null_message = _("There is no selection to transform.");
|
|
|
|
for (iter = objects; iter; iter = iter->next)
|
|
{
|
|
GimpItem *item = iter->data;
|
|
|
|
/* cannot happen, so don't translate these messages */
|
|
if (gimp_item_is_content_locked (item, &locked_item))
|
|
locked_message = "The selection's pixels are locked.";
|
|
else if (gimp_item_is_position_locked (item, &locked_item))
|
|
locked_message = "The selection's position and size are locked.";
|
|
}
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_PATH:
|
|
null_message = _("There is no path to transform.");
|
|
|
|
for (iter = objects; iter; iter = iter->next)
|
|
{
|
|
GimpItem *item = iter->data;
|
|
|
|
if (gimp_item_is_content_locked (item, &locked_item))
|
|
locked_message = _("The selected path's strokes are locked.");
|
|
else if (gimp_item_is_position_locked (item, &locked_item))
|
|
locked_message = _("The selected path's position is locked.");
|
|
else if (! gimp_path_get_n_strokes (GIMP_PATH (item)))
|
|
locked_message = _("The selected path has no strokes.");
|
|
}
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_IMAGE:
|
|
/* cannot happen, so don't translate this message */
|
|
null_message = "There is no image to transform.";
|
|
break;
|
|
}
|
|
|
|
if (! objects)
|
|
{
|
|
g_set_error_literal (error, GIMP_ERROR, GIMP_FAILED, null_message);
|
|
if (error)
|
|
{
|
|
gimp_tools_show_tool_options (display->gimp);
|
|
gimp_widget_blink (options->type_box);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
if (locked_message)
|
|
{
|
|
g_set_error_literal (error, GIMP_ERROR, GIMP_FAILED, locked_message);
|
|
if (error)
|
|
{
|
|
if (locked_item == NULL)
|
|
locked_item = GIMP_ITEM (objects->data);
|
|
|
|
gimp_tools_blink_lock_box (display->gimp, locked_item);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
return objects;
|
|
}
|
|
|
|
gboolean
|
|
gimp_transform_tool_transform (GimpTransformTool *tr_tool,
|
|
GimpDisplay *display)
|
|
{
|
|
GimpTool *tool;
|
|
GimpTransformOptions *options;
|
|
GimpImage *image;
|
|
GList *selected_objects;
|
|
GeglBuffer *orig_buffer = NULL;
|
|
gint orig_offset_x = 0;
|
|
gint orig_offset_y = 0;
|
|
GeglBuffer *new_buffer;
|
|
gint new_offset_x;
|
|
gint new_offset_y;
|
|
GimpColorProfile *buffer_profile;
|
|
gchar *undo_desc = NULL;
|
|
gboolean new_layer = FALSE;
|
|
GError *error = NULL;
|
|
|
|
g_return_val_if_fail (GIMP_IS_TRANSFORM_TOOL (tr_tool), FALSE);
|
|
g_return_val_if_fail (GIMP_IS_DISPLAY (display), FALSE);
|
|
|
|
tool = GIMP_TOOL (tr_tool);
|
|
options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tool);
|
|
image = gimp_display_get_image (display);
|
|
|
|
g_return_val_if_fail (GIMP_IS_IMAGE (image), FALSE);
|
|
|
|
selected_objects = gimp_transform_tool_check_selected_objects (tr_tool, display,
|
|
&error);
|
|
|
|
if (! selected_objects)
|
|
{
|
|
gimp_tool_message_literal (tool, display, error->message);
|
|
g_clear_error (&error);
|
|
return FALSE;
|
|
}
|
|
|
|
gimp_transform_tool_recalc_matrix (tr_tool, display);
|
|
|
|
if (! tr_tool->transform_valid)
|
|
{
|
|
gimp_tool_message_literal (tool, display,
|
|
_("The current transform is invalid"));
|
|
return FALSE;
|
|
}
|
|
|
|
if (GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->recalc_matrix &&
|
|
gimp_matrix3_is_identity (&tr_tool->transform))
|
|
{
|
|
/* No need to commit an identity transformation! */
|
|
return TRUE;
|
|
}
|
|
|
|
if (! gimp_transform_tool_confirm (tr_tool, display))
|
|
return FALSE;
|
|
|
|
gimp_set_busy (display->gimp);
|
|
|
|
/* We're going to dirty this image, but we want to keep the tool around */
|
|
gimp_tool_control_push_preserve (tool->control, TRUE);
|
|
|
|
undo_desc = GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->get_undo_desc (tr_tool);
|
|
gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_TRANSFORM, undo_desc);
|
|
g_free (undo_desc);
|
|
|
|
switch (options->type)
|
|
{
|
|
case GIMP_TRANSFORM_TYPE_LAYER:
|
|
if (! gimp_channel_is_empty (gimp_image_get_mask (image)))
|
|
{
|
|
orig_buffer = gimp_drawable_transform_cut (
|
|
selected_objects,
|
|
GIMP_CONTEXT (options),
|
|
&orig_offset_x,
|
|
&orig_offset_y,
|
|
&new_layer);
|
|
}
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_SELECTION:
|
|
case GIMP_TRANSFORM_TYPE_PATH:
|
|
case GIMP_TRANSFORM_TYPE_IMAGE:
|
|
break;
|
|
}
|
|
|
|
/* Send the request for the transformation to the tool...
|
|
*/
|
|
new_buffer = GIMP_TRANSFORM_TOOL_GET_CLASS (tr_tool)->transform (
|
|
tr_tool,
|
|
selected_objects,
|
|
orig_buffer,
|
|
orig_offset_x,
|
|
orig_offset_y,
|
|
&buffer_profile,
|
|
&new_offset_x,
|
|
&new_offset_y);
|
|
|
|
if (orig_buffer)
|
|
g_object_unref (orig_buffer);
|
|
|
|
switch (options->type)
|
|
{
|
|
case GIMP_TRANSFORM_TYPE_LAYER:
|
|
if (new_buffer)
|
|
{
|
|
/* paste the new transformed image to the image...also implement
|
|
* undo...
|
|
*/
|
|
gimp_drawable_transform_paste (GIMP_DRAWABLE (selected_objects->data),
|
|
new_buffer, buffer_profile,
|
|
new_offset_x, new_offset_y,
|
|
new_layer, TRUE);
|
|
g_object_unref (new_buffer);
|
|
}
|
|
break;
|
|
|
|
case GIMP_TRANSFORM_TYPE_SELECTION:
|
|
case GIMP_TRANSFORM_TYPE_PATH:
|
|
case GIMP_TRANSFORM_TYPE_IMAGE:
|
|
/* Nothing to be done */
|
|
break;
|
|
}
|
|
|
|
gimp_image_undo_group_end (image);
|
|
|
|
/* We're done dirtying the image, and would like to be restarted if
|
|
* the image gets dirty while the tool exists
|
|
*/
|
|
gimp_tool_control_pop_preserve (tool->control);
|
|
|
|
gimp_unset_busy (display->gimp);
|
|
|
|
gimp_image_flush (image);
|
|
|
|
g_list_free (selected_objects);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
gimp_transform_tool_set_type (GimpTransformTool *tr_tool,
|
|
GimpTransformType type)
|
|
{
|
|
GimpTransformOptions *options;
|
|
|
|
g_return_if_fail (GIMP_IS_TRANSFORM_TOOL (tr_tool));
|
|
|
|
options = GIMP_TRANSFORM_TOOL_GET_OPTIONS (tr_tool);
|
|
|
|
if (! tr_tool->restore_type)
|
|
tr_tool->saved_type = options->type;
|
|
|
|
tr_tool->restore_type = FALSE;
|
|
|
|
g_object_set (options,
|
|
"type", type,
|
|
NULL);
|
|
|
|
tr_tool->restore_type = TRUE;
|
|
}
|